Quick Sort(快速排序) 算法原理详见: https://grant1499.github.io/posts/7ff5bb67.html。
算法思想 :
选取一个数为基准(一般取第一个或最后一个或者中间的数)
将比基准小的数交换到前面,比基准大的数交换到后面
对左右区间重复第二步,直到各区间只有一个数
代码实现:
题目链接:https://www.acwing.com/problem/content/description/787/。
注意这里的代码和笔试代码有不同的地方,注意区别。
按照王道的代码,每轮排序后会将基准元素放到其最终位置上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> #include <algorithm> using namespace std ;const int N = 100010 ;int a[N],n;void quick_sort (int a[],int l,int r) { if (l >= r) return ; int i = l-1 ,j = r+1 ,x = a[l+r>>1 ]; while (i < j){ while (a[++i] < x); while (a[--j] > x); if (i < j) swap(a[i],a[j]); } quick_sort(a,l,j),quick_sort(a,j+1 ,r); } int main () { cin >> n; for (int i = 0 ;i < n;i ++) cin >> a[i]; quick_sort(a,0 ,n-1 ); for (int i = 0 ;i < n;i ++) cout << a[i] << ' ' ; return 0 ; }
效率分析:
1.最好时间复杂度:$O(n*log n)$。
每一轮划分都以中位数为基准,递归大概进行log n层。
2.平均时间复杂度:$O(n*log n)$。
递归的平均执行层数是log n层。
3.最坏时间复杂度:$O(n^2)$。
每一轮划分以最小/大值为基准,分为1和n-1两组,执行n^2次。
快排极少 会退化到$O(n^2)$,基本不用考虑。
4.稳定性:不稳定 排序。
5.空间复杂度:$O(n*log n)$。
递归调用系统栈,一般指平均值。
拓展题目:
*acwing.1451. 单链表快速排序 1 2 3 4 5 6 7 8 9 10 11 给定一个单链表,请使用快速排序算法对其排序。 要求:期望平均时间复杂度为 O(nlogn),期望额外空间复杂度为 O(logn)。 思考题: 如果只能改变链表结构,不能修改每个节点的val值该如何做呢? 数据范围 链表中的所有数大小均在 int 范围内,链表长度在 [0 ,10000 ]。 输入样例: [5 , 3 , 2 ] 输出样例: [2 , 3 , 5 ]
链表快排其实比数组快排更好些,数组快排边界处理比较麻烦。
思路:
将整个链表划分为left,mid,right三个子链表后,再递归处理left和right链表。
然后把left,mid,right子链表从左到右拼接起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution {public : ListNode* get_tail (ListNode* head) { while (head->next) head = head->next; return head; } ListNode* quickSortList (ListNode* head) { if (!head || !head->next) return head; auto left = new ListNode(-1 ),mid = new ListNode(-1 ),right = new ListNode(-1 ); auto ltail = left,mtail = mid,rtail = right; int val = head->val; for (auto p = head; p; p = p->next){ if (p->val < val) ltail = ltail->next = p; if (p->val == val) mtail = mtail->next = p; if (p-> val > val) rtail = rtail->next = p; } ltail->next = mtail->next = rtail->next = NULL ; left->next = quickSortList(left->next); right->next = quickSortList(right->next); get_tail(left)->next = mid->next; get_tail(left)->next = right->next; auto p = left->next; delete left; delete mid; delete right; return p; } };
Heap Sort(堆排序) 堆是一棵完全二叉树 。
算法原理详见: https://grant1499.github.io/posts/2a91527b.html。
代码实现:
题目链接:https://www.acwing.com/problem/content/description/787/。
代码以大根堆为例。(小根堆是一样的,见算法基础课)
大根堆定义:每个节点的键值大于或等于左右孩子,堆顶就是整个堆的最大值。
堆一般采用顺序存储,比链式存储效率高不少,因为要用到索引操作。
编号从1开始,则编号为x的左孩子为2x,右孩子为2x+1,父节点为:$\lfloor x/2 \rfloor$。
建堆 :时间复杂度,O(n)。证明见算法基础课 。
从倒数第2层直到最上层执行down操作,向下调整建堆。
倒数第2层节点数约n/4,往下down 1层;倒数第3层节点数约n/8,往下down 2层;……
总时间复杂度约为:n/4 * 1+ n/8 * 2 + n/16 * 3 + ... = O(n)
。
之所以能O(n)建堆,是因为堆性质很弱,二叉堆并不是唯一的。
向下调整down :时间复杂度,$O(log n)$。完全二叉树的层数约为log n。
在该结点的儿子中,找一个最小的,与该结点交换,重复此过程直到底层 。
向上调整类似。可用迭代或者递归实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include <iostream> #include <algorithm> using namespace std ;const int N = 100010 ;int a[N],n;void down (int a[],int u,int len) { int t = u; if (2 *u <= len && a[2 *u] > a[t]) t = 2 *u; if (2 *u+1 <= len && a[2 *u+1 ] > a[t]) t = 2 *u+1 ; if (t != u){ swap(a[u],a[t]); down(a,t,len); } } void heap_sort (int a[],int n) { int len = n; for (int i = n/2 ; i; i --) down(a,i,len); for (int i = 0 ;i < n-1 ;i ++){ swap(a[1 ],a[len]); len --; down(a,1 ,len); } } int main () { cin >> n; for (int i = 1 ;i <= n;i ++) cin >> a[i]; heap_sort(a,n); for (int i = 1 ;i <= n;i ++) cout << a[i] << ' ' ; return 0 ; }
效率分析:
1.最好时间复杂度:$O(n*log n)$。
2.平均时间复杂度:$O(n*log n)$。
3.最坏时间复杂度:$O(n*log n)$。
背过就行。
4.稳定性:不稳定 排序。
堆顶和堆尾元素存在交换,会导致不稳定。
Merge Sort(二路归并排序) 算法原理详见: https://grant1499.github.io/posts/7ff5bb67.html。
算法思想 :
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
代码实现:
题目链接:https://www.acwing.com/problem/content/description/787/。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> #include <algorithm> using namespace std ;const int N = 100010 ;int a[N],temp[N],n; void merge_sort (int a[],int l,int r) { if (l >= r) return ; int mid = l + r >> 1 ; int i = l,j = mid + 1 ,k = 0 ; merge_sort(a,i,mid),merge_sort(a,j,r); while (i <= mid && j <= r){ if (a[i] <= a[j]) temp[k++] = a[i++]; else temp[k++] = a[j++]; } while (i <= mid) temp[k++] = a[i++]; while (j <= r) temp[k++] = a[j++]; for (int i = 0 ,j = l;j <= r;i ++,j ++) a[j] = temp[i]; } int main () { cin >> n; for (int i = 0 ;i < n;i ++) cin >> a[i]; merge_sort(a,0 ,n-1 ); for (int i = 0 ;i < n;i ++) cout << a[i] << ' ' ; return 0 ; }
效率分析:
1.最好时间复杂度:$O(n*log n)$。
2.平均时间复杂度:$O(n*log n)$。
3.最坏时间复杂度:$O(n*log n)$。
二路归并每次都将数组分为一半,大概执行O(log n)层。
4.稳定性:稳定排序。
5.空间复杂度:O(n)。
需要一个额外的临时数组。
快排与归并的区别:
快排:先处理,再递归。(尾递归,可以转化为迭代写法)
归并:先递归,再处理。
快排递归划分的期望是O(logn),而归并递归划分一定是O(logn)。