【C++】快速排序以及归并排序的算法总结及思想应用分析

两种算法都是采用分治的思想,将大问题分解为一个个小问题,再逐个解决。

1 快速排序

思想

首先是快排,最坏时间复杂度是O(n²),平均的复杂度是O(nlogn),原理如下:

  1. 先在当前区间利用partition()思想:从无序数组中随机选一个基点(实际应用常取该区间的第一个数作为基点),然后通过一趟扫描,以基点为分界线将数组中其他元素分为两部分,使得左边部分的数小于等于基点,右边部分的数大于等于基点(左部分或者右部分都可能为空),最后返回 基点 在新的数组中的位置。
  2. 然后再分别对这个基点左侧区间和右侧区间进行上述处理,递归下去即可完成排序
  3. 注意,因为我们基点通常选择最左边的,因此应该先缩右边,即先处理--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)。原理如下:

  1. 将当前区间不断二分,直到左右区间中只有一个数字时,视其为有序区间,两两进行合并
  2. 两个有序区间合并时,利用双指针的思想将两个有序区间合并成一个有序区间
  3. 然后将两两的有序区间不断合并,最终完成排序

代码(双指针分治排序+递归)

第一步和第三步如下:


 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 相同点与区别

相同点

都利用了分治的思想,具体实现都可以使用递归来完成。

区别

  • 空间上
    • 快速排序是原地排序
    • 归并排序不是原地排序(因为两个有序数组的合并一定需要额外的空间协助才能合并)
  • 分治的思路
    • 快排每次将数组一分为三(把排好序的那个数字摘出来)
    • 归并排序每次将数组一分为二
  • 排序的方向
    • 归并是自上而下的分解,接着再自下而上的合并排序。
    • 快排是边分解边排序,是自上而下的排序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值