关于排序的几种经典算法总结(持续更新~~2021.06.02)

排序方法总结--后序补充稳定性概念和其他集中排序方法的概念,包括堆排序

前言:
  关于排序我觉得有篇博客讲的比较全面,有空可以看看分类刷题(三):排序(Sort) ,本文的话主要介绍面试场用的快速排序、冒泡排序和归并排序。

1.快速排序

快速排序其实是在冒泡排序的基础上做出的一个改进。快速排序的基本思想是:
  1、先从数列中取出一个数作为基准数,一般是数列最左边的start
  2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
  3、再对左右区间重复第二步,直到各区间只有一个数
  首先我们先建立一组数据:主要有三个参数,i为区间的开始地址,j为区间的结束地址,X为当前的开始的值.

1.1搜索流程

在快速排序中我们先以一个例子来分析,就是我们的X是我们的start值,我们要将X记录下来,然后一开始从end开始,我们知道我们需要让X右侧都是比他大的元素,所以不断减少end的下标,如果比X大那符合要求就走,注意到end走的过程越多或者说后面出现的start过程走得越多,对我们的计算是好事,意味着走过的路段都是可行解了,在以后的搜索中都不用考虑了。极端情况X就是最小的值,他就在最左边,导致end–一直到start<end!!牢记
  前面说了一些额外的话,好,我们继续刚才的搜索,end一直减少,直到end下标对应的数值假设是Y比X要小了,好!出现不速之客了,小的必须在我左边,所以呢我就把X和Y呼唤了,这样达到了什么功能!注意到当前X右侧都是没问题的,包含X点当前Y左侧都是没问题的,包含Y点。
  接下来需要继续对start-end的区间进行搜索了(开区间)。因为之前搜素了下右侧,现在我搜索下左侧了,start不是没问题么,那start++,判断这个下标对应的数是否比end小,或者说比X小,如果小,没问题继续+,如果start++出现对应的数,假设是M,M>X,好! 出现不速之客了,必须在X右侧才对啊,那就把M和X互换,这时候X在end位置,说白了就是把end的值变成M。此时可以看到左侧收缩了,右侧还没收缩呢。
  然后重复上述操作start和end之间距离不断减少,一旦出现end–到start=end或者 start++到start=end,注意到,我们说某一个区间左右都是闭合没问题的,一个边界移动另一个边界不动,重合了说明都没问题啊。而且重合的点肯定是原来的X值! 此时当start=end时,排序结束。
  之前说的是互换,其实我们也可以不换,把start++ 或者end–遍历到出问题的元素覆盖到end或者start就行,去而他本来的位置应该让X来填补,我们就不填补了,因为我们的X每次都会转移转移,最终转移到start=end,我直接在这里地方给他赋值不就行了!
  注意最后的start就是我们的X在原始数组中的下标。因为左边都是比他小,右边都是比他大。我们能获得原始数组中从小大到排名第start+1的元素就是nums[start]!利用这个方法,我们每次排序返回index,比如我们想找从小到大第五个元素是谁。先对nums[1]做基准,能找出他的排名,比如是4 ,4<5,那基准数在数组右侧,对右侧区间某个数做基准继续找他的排名。直到找到排名等于5即可。这个方法可以用来求Leetcode215题:数组中的第K个最大元素:

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        if(nums.empty()||k<=0||k>nums.size())
            return - 1;
        k = nums.size()-k; //我们是升序排序,第K个最大的反过来就是这个值,索引从0开始~
        int index = 0;
        int start = 0;
        int end = nums.size()-1;
        while(1)
        {
            index = quick(nums,start,end);
            if(index==k)  //直到索引到这个index就结束~
                break;
            else if(index<k)
                start = index +1;
            else 
                end = index -1;
        }
        return nums[index];
    }
   //快排分割函数
  int quick(vector<int>&nums,int i,int j)
    {
        int start = i;
        int end = j;
        int temp =  nums[start];
        while(start<end)
        {
            while(nums[end]>temp && start<end)
            {
                --end;
            }
            if(start<end)
            {
                nums[start]=nums[end];
                ++start;
            }
            while(nums[start]<temp && start<end)
                ++start;
            if(start<end)
            {
                nums[end]=nums[start];
                --end;
            }
        }
        nums[start] = temp;
        return start;
    }
};

除了上面的传统方法,我们基准数字可以随机选择一个,然后换到最右边,可以采用下面的程序方法:

class Solution {
public:
    int quickSelect(vector<int>& a, int l, int r, int index) {
        int q = randomPartition(a, l, r);
        if (q == index) {
            return a[q];
        } else {
            return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
        }
    }

    inline int randomPartition(vector<int>& a, int l, int r) {
        int i = rand() % (r - l + 1) + l; //rand()产生0-最大整数。这样会产生0到 r-l的整数。
        swap(a[i], a[r]); //一般把基准值放在开头或者结尾,调入参数的时候方便点
        return partition(a, l, r);
    }

    inline int partition(vector<int>& a, int l, int r) {
        int x = a[r], i = l - 1;
        for (int j = l; j < r; ++j) {
            if (a[j] <= x) {
                swap(a[++i], a[j]);
            }
        }
        swap(a[i + 1], a[r]);
        return i + 1;
    }

    int findKthLargest(vector<int>& nums, int k) {
        srand(time(0));
        return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
    }
};

排序的核心程序如下,下面对这块程序做个解释:
我们将目标值放到数组最右边,x=a[r],然后j一直移动嘛,最多移动到r的左边,最右边是我们的目标值当然放到循环之后操作啦。这里i其实记录了我们已经完成从左到右的排序指标索引。一开始一个都没有所以i=l-1。然后j一直再变大,如果对应j的元素比目标值小,符合要求,那么我们成功搞完了一个数,索引i也增大一个,如果全部都是比我们最右边的数字小,那每次i都是后来加上去然后换,那i=j相当于换了个寂寞! 注意到如果遇到比我们目标值大的元素,j在变换的过程中i是不变的。知道到了r的位置,那么i+1我们知道的从工作到右第一个比目标值大的元素! 那就和我们目标值换位置不就行了。如果中间遇到符合条件得了,那i+1的值就换成那个符合条件的就行了,交换嘛。对于我们的最终目标反正肯定在i后面!

 inline int partition(vector<int>& a, int l, int r) {
        int x = a[r], i = l - 1;
        for (int j = l; j < r; ++j) {
            if (a[j] <= x) {
                swap(a[++i], a[j]);
            }
        }
        swap(a[i + 1], a[r]);
        return i + 1;
    }

1.2对整个数组进行快排的程序和模板!!:

之前我们的操作只叙述了对基准点进行快排后的结果。就是选择数组中一个数,快排后数组左侧都是比他小,右侧都是比他大的。那怎么实现整个数组的快速排序呢!不多说,先上模板(自己写的):

//先写一个工具人函数,他的目的是实现对不同区域的快排的组织,中介环节
void QuickSort(vector<int>& a, int low, int high)
{
    if(high >= low) //或者说是 high==low
    {
        return;
    }
    int j = Partition(a, low, high); //快排的核心函数~~ 
    QuickSort(a, low, j-1); //将小于a[j]的部分进行排序
    QuickSort(a, j+1, high); //将大于a[j]的部分进行排序
}
//在工具人函数中需要调用核心函数,也就是单次快排的方式函数
int Partition(vector<int>& a, int low, int high){
     int i = rand() % (r - l + 1) + l; //rand()产生0-最大整数。这样会产生0到 r-l的整数。
     swap(a[i], a[r]); //一般把基准值放在开头或者结尾,调入参数的时候方便点
     int x = a[r], i = l - 1;
     for (int j = l; j < r; ++j) {
         if (a[j] <= x) 
            swap(a[++i], a[j]);
        }
        swap(a[i + 1], a[r]);
        return i + 1;
}

记住这个模板把,补充下,核心快排分割函数还可以用这种形式:

int quick(vector<int>&nums,int i,int j)
    {
        int start = i;
        int end = j;
        int temp =  nums[start];
        while(start<end)
        {
            while(nums[end]>temp && start<end)
            {
                --end;
            }
            if(start<end)
            {
                nums[start]=nums[end];
                ++start;
            }
            while(nums[start]<temp && start<end)
                ++start;
            if(start<end)
            {
                nums[end]=nums[start];
                --end;
            }
        }
        nums[start] = temp;
        return start;
    }

第三种:

void quickSort(int strs[], int l, int r)
{
	 if(l >= r) return;
        int i = l, j = r;
        while(i < j) {  //注意到这个方法下面事大于等于或者小于等于必须加等号,不然遇到111会先入死循环  就是表明左侧筛选大于等于的元素,右侧小于等于的元素,最终整合了就是完美的
            while(strs[j]>=strs[l]  && i < j) j--; //右边搜索遇到第一个刺头比他小
            while(strs[i] <=strs[l] && i < j) i++; //左边搜索遇到第一个刺头比他大
            swap(strs[i], strs[j]); //这两个刺头一换位置就完美了,然后走过的路都是完美的,进入下次大循环  直到i=j
        }
        swap(strs[i], strs[l]);  //i只所及都是小于等于他的元素!没问题
        quickSort(strs, l, i - 1);
        quickSort(strs, i + 1, r);
}

2.归并排序

Leetcode21:合并两个有续链表求解方法中就利用了归并排序的的思想。该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。可以用下面这张图来概括分治的过程,略去递归就看最后一次排序,左右两个子部分都是已经升序排好了的,我们可以创建一个temp数组,对左右两部分建立双指针,小的进数组指针移动,每次比较ij两个指针对应元素,小的进,然后移动,大的不进不移动。一旦某个走完,数组下面的给没走完的接盘。这就是分治算法。
在这里插入图片描述

2.1归并排序模板!

下面是归并排序的模板程序,根据上面的思路,看起来也是很好理解的。和快速排序一样,都是存在一个空壳工具人函数,工具人函数里面包含了对单次排序的核心函数。递归过程有点像二叉树的后序遍历。如何理解呢? MergeArray函数实现对当前low high范围内以mid为分解构成的两个有序子部分进行合并排序。这属于操作。区间分别为[low,mid],[mid+1,high]。那操作之前我们需要将左右区间做一个有序排序,这个是怎么做到的。既然是递归我们就不用想了,直接调用MergeSort(nums, low, mid);MergeSort(nums, mid + 1, high); 因为我们的目的是让;MergeSort函数实现对输入的左右区间内元素实现排序,好!那我在整体排序之前需要对左右小区域排序。等这两个递归出来已经排好了。而递归的过程呢又是进去后再细分细分,直到low=high的时候或者high降低到high=low的时候结束跳出,一般这个时候比如low=1,high=2 就会跳出进入一次操作了。然后实现2个元素的排列。最差的一次就是2个元素能排好。然后不断排,排排~

class Solution {
public:
    void MergeArray(vector<int>& nums, int low, int high){
        vector<int> temp(high - low + 1, 0);
        int mid = (low + high)/2, k = 0;
        int i = low, m = mid, j = mid + 1, n = high;
        while(i <= m && j <= n){
            if(nums[i] <= nums[j]) temp[k++] = nums[i++];
            else temp[k++] = nums[j++];
        }
        while(i <= m) temp[k++] = nums[i++];
        while(j <= n) temp[k++] = nums[j++];
        for(int p = 0; p < temp.size(); p++)
            nums[low + p] = temp[p];
    }
    void MergeSort(vector<int>& nums, int low, int high){
        if(low < high){
            int mid = (low + high)/2;
            MergeSort(nums, low, mid);
            MergeSort(nums, mid + 1, high);
            MergeArray(nums, low, high);
        }
    }
    void sortIntegers(vector<int>& nums) {
        MergeSort(nums, 0, (int)nums.size() -1);
    }
};

3.冒泡排序

冒泡排序,顾名思义,就是大的值给你先冒出来。每一趟确定一个元素,一趟扫描采取相邻元素比较的形式进行。时间复杂度O(n2),具体的原理是什么呢?注意到每次比较相邻元素,大的就右移,可以看到最大的元素肯定会不断右移到最右边,所以每次都能确定一个最大元素到数组最右侧。循环nums.size()-1次就能确定所有的元素了!

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        int numsSize = nums.size();
        //每一趟确定一个元素,总共numsSize-1趟
        for (int i = 1; i < numsSize; ++i){
            //每次比较两个相邻元素,出现逆序则交换,就是把大的元素逐渐移动到最后(故名为冒泡)
            for (int j = 0; j + 1 < numsSize; ++j){
                if (nums[j] > nums[j + 1]){//出现逆序,交换
                    int tempVal = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = tempVal;
                }
            }
        }
        return nums;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值