两种算法都是采用分治的思想,将大问题分解为一个个小问题,再逐个解决。
1 快速排序
思想
首先是快排,最坏时间复杂度是O(n²)
,平均的复杂度是O(nlogn)
,原理如下:
- 先在当前区间利用
partition()
思想:从无序数组中随机选一个基点
(实际应用常取该区间的第一个数作为基点
),然后通过一趟扫描,以基点
为分界线将数组中其他元素分为两部分,使得左边部分的数小于等于基点
,右边部分的数大于等于基点
(左部分或者右部分都可能为空),最后返回基点
在新的数组中的位置。 - 然后再分别对这个
基点
左侧区间和右侧区间进行上述处理,递归下去即可完成排序 - 注意,因为我们基点通常选择最左边的,因此应该先缩右边,即先处理
--j
while(i < j && arr[i] <= arr[l]) i++;
while(i < j && arr[j] >= arr[l]) j–;
文中的写法这两个 while执行完, i j 同时指向一个 < arr[l] 的数,因此最后再执行 arr[l], arr[i] = arr[i], arr[l] 可以把哨兵交换到正确的位置。 而如果互换这两句,那么就是 i 先向右遍历,两个 while 执行完, i j 同时指向一个 > arr[l] 的数,那么就不对了。如果要交换写,那么同时也要把哨兵换成数组的末元素,让整个哨兵划分操作对称。
代码(双指针交换的partition + 递归)
void quickSort(vector<int> &num, int left, int right)
{
//第一步的patition
if(left >= right) return;
int i = left, j = right;
while(i < j) {
//这里把每个区间的第一个数作为一个基点
//利用双指针,将不符合要求的两个部分交换
//要记得因为左节点做基准,因此左侧基点应为小于等于
while(i < j && num[left] <= num[j]) --j;
while(i < j && num[left] >= num[i]) ++i;
swap(num[i], num[j]);
}
//最后相当于已经找到了基准点所应该在得位置
swap(num[i], num[left]);
//第二步的递归
quickSort(num, left, i - 1);
quickSort(num, i + 1, right);
}
链表版
递归法,仅交换节点的值
class Solution {
ListNode* quickSortList(ListNode* head) {
if (!head) return head;
quickSort(head, NULL);
return head;
}
void quickSort(ListNode* head, ListNode* tail) {
if (head == tail || head->next == tail) return ;
int key = head->val;
ListNode *p = head, *q = p->next;
while (q != tail) {
if (q->val < key) {
p = p->next;
swap(p->val, q->val);
}
q = q->next;
}
if (p != head)
swap(head->val, p->val);
quickSort(head, p);
quickSort(p->next, tail);
}
ListNode* sortList(ListNode* head) {
return quickSortList(head);
}
};
应用(topK)
这个题正常就是直接快排
取第K个即可,但是快排的时间复杂度为nlogn
,因此,我们可以采用partition的思想,二分地只排前n个,即可在n
的复杂度内搞定。因为每次快排中的partition都会找到一个数应该在的位置,随后再递归左边和右边,而我们可以根据排好序的位置,选择性的递归左边或者右边,可以少递归一半!
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
sort(arr.begin(), arr.end());
return vector<int>(arr.begin(),arr.begin()+k);
}
void quick_sort(vector<int>& arr, int k, int l, int r) {
int i = l, j = r;
while(i < j) {
while(i < j && arr[l] <= arr[j]) j--;
while(i < j && arr[l] >= arr[i]) i++;
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
//此处二分
if(i > k) quick_sort(arr, k, l, i-1);
else if (i < k) quick_sort(arr, k, i+1, r);
return;
}
};
2 归并排序
思想
归并排序使用二分和递归,采用的也是分治的思想,但是其最坏、平均时间复杂度均为O(nlogn)
。原理如下:
- 将当前区间不断二分,直到左右区间中只有一个数字时,视其为有序区间,两两进行合并
- 两个有序区间合并时,利用双指针的思想将两个有序区间合并成一个有序区间
- 然后将两两的有序区间不断合并,最终完成排序
代码(双指针分治排序+递归)
第一步和第三步如下:
void merge_sort(vector<int>& nums, int l, int r) {
if(l >= r) return;
int mid = l + (r - l) / 2;
//先分成左右区间,到了底部再合并递归回来
merge_sort(nums, l, mid);
merge_sort(nums, mid+1, r);
merge(nums, l, mid, r);
}
第二步核心的合并代码(双指针,分治合并)如下:
void merge(vector<int>& nums, int l, int mid, int r) {
//因为左边和右边区间都排好序了,因此同时从左到右取最小的即可
int p_l = l, p_r = mid + 1;
//tem为临时数组,用来放合并后的结果
vector<int>tem;
while(p_l <= mid && p_r <= r) {
tem.push_back(min(nums[p_l], nums[p_r]));
if(tem.back() == nums[p_l]) ++p_l;
else if(tem.back() == nums[p_r]) ++p_r;
}
//当左边区间有剩余时,放进tem即可
while(p_l <= mid) tem.push_back(nums[p_l++]);
while(p_r <= r) tem.push_back(nums[p_r++]);
//赋值回之前的函数
for(auto &n: tem) nums[l++] = n;
}
应用(逆序对)
其实分治合并的过程中,正常不是逆序的话,应该是左区间的小于右区间的,而一旦先取了右边的放入tem
数组中,那么就代表着左边区间剩余的n个数
是大于这个右区间的数的,因此需要cnt += mid - p_l + 1
来统计。这些也正好就是逆序对!
class Solution {
public:
int cnt = 0;
int reversePairs(vector<int>& nums) {
int n = nums.size();
merge_sort(nums, 0, n-1);
return cnt;
}
void merge_sort(vector<int>& nums, int l, int r) {
if(l >= r) return;
int mid = l + (r - l) / 2;
merge_sort(nums, l, mid);
merge_sort(nums, mid+1, r);
merge(nums, l, mid, r);
}
void merge(vector<int>& nums, int l, int mid, int r) {
int p_l = l, p_r = mid + 1;
vector<int>tem;
while(p_l <= mid && p_r <= r) {
tem.push_back(min(nums[p_l], nums[p_r]));
if(tem.back() == nums[p_l]) ++p_l;
else if(tem.back() == nums[p_r]) {
++p_r;
//多一行代码即可
cnt += mid - p_l + 1;
}
}
while(p_l <= mid) tem.push_back(nums[p_l++]);
while(p_r <= r) tem.push_back(nums[p_r++]);
for(auto &n: tem) nums[l++] = n;
}
};
3 相同点与区别
相同点
都利用了分治的思想,具体实现都可以使用递归来完成。
区别
- 空间上
- 快速排序是
原地排序
- 归并排序
不是原地排序
(因为两个有序数组的合并一定需要额外的空间协助才能合并)
- 快速排序是
- 分治的思路
- 快排每次将数组
一分为三
(把排好序的那个数字摘出来) - 归并排序每次将数组
一分为二
- 快排每次将数组
- 排序的方向
- 归并是自上而下的分解,接着再自下而上的合并排序。
- 快排是边分解边排序,是自上而下的排序。