赞
踩
单链表的快速排序与归并排序
[2024-03-10: 今天试着重写了单链表的快速排序。觉今是而昨非。
下文的第一部分虽然代码很短(20行),但严格讲,不算是单链表的快速排序;而下文的第三部分,虽然算是单链表快速排序了,也很好理解,可是代码有近60行!且递归的函数中用了2个额外的局部变量,造成了内存的浪费。
而今天重写的这个版本,也还算好理解,代码行数减少到39行左右(其实还能减,不过为了可读性,先不减了),也没有了额外的局部变量。鉴于当前文章已经很长,就不贴在这里了,另开一贴《重写单链表的快速排序》 。
[2022-04-04:
本文再次更新。至此,本文结构如下:
第一部分: 介绍一种单链表的快速排序方法,缺点是略难理解和记忆,优点是短小精悍;(写于2018年)
第二部分:介绍一种单链表的归并排序方法,也略难实现,优点是性能远高于其他2种方法;(写于2021-11)
第三部分:介绍的是另一种单链表的快速排序,优点是很好理解与实现。(写于2022-04)
第四部分:比较上述三种方法。(写于2022-04)
]
首先,很容易想到的是:
第二个问题比较好回答,只要知道子链表的首尾节点,就可以做递归了。伪代码是:
void quick_sort_link(Node *start, Node *end=NULL);
第一个问题才是要解决的难题。思路如下:
假设第一轮基准值定位做完了,我们需要有什么才能继续进行?
很显然,需要有左子链表和右子链表的各自的首尾节点。那么,左链表的首节点和右链表的尾节点,这2个一开始就有了。所以,需要有的是:左子链表的尾节点,和 右子链表的首节点。而这2个节点分别位于基准值节点的左边和右边。
这个时候,有一个思路是:使用2个辅助指针 p1 和 p2.
p1 是左子链表的最后一个节点,负责维护左子链表;
p2 是右子链表的最后一个节点,负责维护右子链表:它不断右移;其实,相当于p2在不断扩充右子链表,而待探索区不断缩小
最后,还需要交换基准值和p1的值,因为,基准值从来没有动过,还在第一个节点的位置,而p1最终已经指向左子链表的最后一个位置,因此需要交换它们2个。
核心代码就是
int t = start->data; // 基准值
while (p2 != end) {
if (p2->data < t) {
p1 = p1->next;
swap(p1->data, p2->data);
}
p2 = p2->next;
}
swap(start->data, p1->data);
问题是:按上面的算法,初始状态也是正确的吗?
这个时候可以举几个例子来验证了!(这是白板编程时的重要方法!)
比如:
15 -> 1 -> 20, *p1=15, *p2=1,
15 -> 1 -> 20, *p1=1, *p2=20,
15 -> 1 -> 20, *p1=1, *p2=NULL
此时,需交换start和p1的值,即:
1-> 15 -> 20
验证成功!
完整的代码是:
#include <iostream> using namespace std; // int array[] = {34, 54, 23, 12, 23, 99, 45, 89, 99, 13, 14, 100}; // 12, 13, 14, 23, 34, 45, 54, 89, 99, 100 int array[] = {10, 2, 50, 3, 20}; struct Node { int data; struct Node* next; Node(int d):data(d), next(NULL){} }; void print_list(Node* head) { while (head != NULL && head->next != NULL) { cout << head->data << "->"; head = head->next; } if (head && head->next == NULL) { cout << head->data << endl; } } Node* create_list(int* array, int size) { if (size == 0) return NULL; int i = 0; Node* head = new Node(array[i++]); Node* p = head; while (i < size) { p->next = new Node(array[i++]); p = p->next; } return head; } /* 基准值是start->data; 将原链表看作2个链表:左链表和右链表,左链表最后一个节点就是基准值 p1是左链表的最后一个节点,p2是右链表的最后一个节点 因此,当遇到大于基准值的时候,p2一直右移; 当遇到小于基准值的时候,p1右移一个,再交换p1和p2的值,相当于维持了p1和p2的定义 一轮循环的最后,p2到达了end的位置,此时,应该交换p1和start节点的值,这时才是真正的一轮处理的结束 下一轮,就递归调用 qs(start, p1) 和 qs(p1->next, end) 了。 */ void quick_sort_list(Node* start, Node* end=NULL) { if (start == NULL || start == end) return; Node* p1 = start; Node* p2 = start->next; while (p2 != end) { if (p2->data < start->data) { p1 = p1->next; swap(p1->data, p2->data); } p2 = p2->next; } swap(p1->data, start->data); quick_sort_list(start, p1); quick_sort_list(p1->next, end); } int main() { int size = sizeof(array)/sizeof(int); Node* head = create_list(array, size); print_list(head); quick_sort_list(head); print_list(head); return 0; }
以上内容基本是多年前写的;但是程序仍有一个比较大的缺陷,就是上面是用swap交换的节点内容,而不是交换节点。接下来介绍单链表的归并排序。
算法是:将一条链表,先找到中间节点,分成2个链表,然后不断递归地细分下去,直到“一节点即为一链表”为止;最后一路合并回最终的一条链表。
说来简单,中间还是有一些小的技巧和边界条件的考虑的。程序如下(注:LeetCode第148题)
struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} }; class Solution { public: ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { if (l1 == nullptr && l2 == nullptr) return nullptr; if (l1 && l2 == nullptr) return l1; if (l1 == nullptr && l2) return l2; ListNode * head = nullptr; ListNode * p = nullptr; while(l1 && l2) { if (l1->val > l2->val) swap(l1, l2); if (head) { p->next = l1; p = p->next; l1 = l1->next; } else { // head is nullptr head = l1; p = head; l1 = l1->next; } } if (l1) p->next = l1; else p->next = l2; return head; } ListNode * _sort(ListNode * beg, ListNode * end) { if (beg == nullptr || beg == end || beg->next == nullptr) return beg; ListNode * p1 = beg; ListNode * p2 = beg->next; // note here, a skill // find out the middle node of current list while (p2!=end) { p1 = p1->next; p2 = p2->next; if (p2 == end) break; p2 = p2->next; } // p1 is the middle node, p1->next is the start node of the second list ListNode * secondStart = p1->next; p1->next = nullptr; ListNode * h1 = _sort(beg, p1); ListNode * h2 = _sort(secondStart, end); ListNode * h = mergeTwoLists(h1, h2); return h; } ListNode* sortList(ListNode* head) { if (head == nullptr || head->next == nullptr) return head; return _sort(head, nullptr); } };
第三种方法的原理是:
具体代码如下:
#include <iostream> using namespace std; struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} }; void print_list(ListNode * head) { if (head == nullptr) return; do { cout << head->val << " "; head = head->next; } while(head); cout << endl; } ListNode* sortList(ListNode* head) { if (head == nullptr || head->next == nullptr) return head; ListNode * targetNodePtr = head; head = head->next; targetNodePtr->next = nullptr; ListNode tmpLeftHead; ListNode tmpRightHead; ListNode * pLeft = &tmpLeftHead; ListNode * pRight = &tmpRightHead; ListNode * p = head; ListNode * tmpNext = nullptr; // Split the rest part of the list to 2 lists while (p != nullptr) { tmpNext = p->next; if (p->val < targetNodePtr->val) { pLeft->next = p; pLeft = pLeft->next; pLeft->next = nullptr; } else { // p->val >= targetNodePtr->val pRight->next = p; pRight = pRight->next; pRight->next = nullptr; } p = tmpNext; } // Process the 2 lists recursively ListNode * leftHead = nullptr; ListNode * rightHead = nullptr; if (tmpLeftHead.next) leftHead = sortList(tmpLeftHead.next); if (tmpRightHead.next) rightHead = sortList(tmpRightHead.next); ListNode * leftListEnd = leftHead; while(leftListEnd) { if (leftListEnd->next == nullptr) break; leftListEnd = leftListEnd->next; } ListNode * finalHead = nullptr; // merge the 2 lists and the original head node if (leftListEnd) { leftListEnd->next = targetNodePtr; targetNodePtr->next = rightHead; finalHead = leftHead; } else { targetNodePtr->next = rightHead; finalHead = targetNodePtr; } return finalHead; } int main() { ListNode n1(1), n2(4), n3(3), n4(2), n5(5), n6(2); n1.next = &n2; n2.next = &n3; n3.next = &n4; n4.next = &n5; n5.next = &n6; print_list(&n1); ListNode * pn = sortList(&n1); print_list(pn); return 0; }
下面看一下具体的执行时间。给定5万个乱序节点:
// way-1
quick_sort_list() took 1.17135 seconds.
quick_sort_list() took 1171351 microseconds.
// way-2
mergeSortList() took 0.00188763 seconds.
mergeSortList() took 1887 microseconds.
// way-3
sortList() took 2.44667 seconds.
sortList() took 2446665 microseconds.
由上可见,
为什么归并排序这么快呢?
从算法的角度来分析,归并排序和快速排序都是
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n), 并无差别,怎么会差3个数量级呢?
笔者仔细看了一下代码,觉得奥秘就在于这个归并算法的实现里全是指针操作。笔者猜测,这些指针应该都是存储在寄存器或临近CPU的cache里,因此操作起来极快;
相反,第一种单链表的快速排序需要交换节点的值,这需要对间接访问的内存进行读写,而第二种单链表快速排序可能因为涉及到临时节点的存取,也会涉及到对内存的读写,因此很可能也就影响到了速度。
如此看来,这个归并算法的实现还是蛮切合计算机体系结构的。
(完)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。