【算法】快速排序、桶排序

快速排序

简单易懂的一篇博客

快排思想

不断的选定一个基准(pivot),然后把比基准数小的放在基准数的左边,把基准数大的都放在基准数的右边,最终找到了基准在数组中的正确索引位置(分区过程partition)。然后递归的对这两部分记录继续快排,以达到整个序列有序。

第一轮快排: 先选出第一个数作为基准pivot,存到临时空间中,同时这个索引也是low。然后双指针索引low和high。第一次从high往前找,如果比基准大,不用管,继续往前挪high–;如果更小,那这个数不应该呆在基准的右边,将它直接保存到当前low位置上。然后从low开始往后遍历,不断low++,直到遇到比基准大的,就将它保存到high位置。不断重复直到low和high相遇(low==high),此时就是基准在数组中的正确索引位置,将基准值保存到这里。

时间复杂度: 快速排序的运行时间与基准划分是否对称有关。

  • 最坏情况下,每次划分过程产生两个区域分别包含n-1个元素和1个元素,即每次的选到的基准刚好都是最大值或最小值,一共需要划分n−1 次,而一次划分需要线性的时间复杂度。所以其时间复杂度会达到O(n^2)。变成了冒泡排序。
  • 在最好的情况下,每次划分所取的基准都恰好是中值,即每次划分都产生两个大小为n/2的区域。此时,快排的时间复杂度为O(nlogn)。所以基准的选择对快排而言至关重要。
  • 平均为O(nlogn)

空间复杂度: O(nlogn)

应用:

  • 《剑指offer》求数组中最小的k个数

如何选择基准数

  • 固定基准:以元素x = a[low]作为划分的基准。
  • 随机
  • 选取数组开头,中间和结尾的元素,通过比较,选择中间的值作为快排的基准

快速排序的4种优化

一篇不错的博客

代码

//对原数组[low,high]进行分区操作,选定一个基准b,并找到基准在数组中的正确索引位置并返回
int partition(vector<int> &nums, int low, int high){
    int pivot = nums[low]; //基准选取策略:固定[low]位置为基准
    while(low<high){
        //选取了low位置为基准,那第一次遍历必须从high往前找
        while(low<high && nums[high]>=pivot){
            high--;
        }
        //找到了一个比pivot小的数
        nums[low] = nums[high];
        //从low往后找
        while(low<high && nums[low]<=pivot){
            low++;
        }
        //找到了一个比pivot大的数
        nums[high] = nums[low];
    }
    nums[low] = pivot; //将基准数放到正确索引位置
    return low; //并返回这个索引位置
}
//直接对原数组[low,high]进行快排操作
void quickSort(vector<int> &nums, int low, int high){
    if(low<high){
        //选择一个基准pivot(注意策略),把比基准数小的放在基准数左边,把基准数大的都放在基准数右边,最终找到基准在数组中的正确索引位置并返回
        int pos = partition(nums, low, high);
        //递归的对左右两个记录进行快排,直到两个指针相遇low==high
        quickSort(nums, low, pos-1);
        quickSort(nums, pos+1, pos);
    }
}

桶排序

优秀博客参考

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为O(nlogn),空间复杂度O(1),是不稳定排序。

基本思想

  • 构建大顶堆:将当前无序序列构建成一个堆,升序排序就构建大顶堆(降序就构建小顶堆)
  • 交换下沉:将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
    重新调整堆结构,使其满足大顶堆定义,反复执行调整+交换步骤,直到整个序列有序。

什么是堆

堆是完全二叉树。当我们对堆中的结点按层进行编号[0,n-1],将这种逻辑结构映射到数组:

  • 大顶堆:每个结点的值都大于或等于其左右孩子结点的值,即arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
  • 小顶堆:每个结点的值都小于或等于其左右孩子结点的值,即arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

如何调整一个堆为大顶堆?

  • 从最后一个非叶子结点开始start=n/2-1,从下往上构建大顶堆即for循环:buildMaxHeap(arr, start, end)。(注意我们的最大值会下沉到原数组末端,所以参数给出了数组的排序范围end)
  • 对于每次从非叶子结点的构建,需要将当前非叶子结点和它的两个左右孩子比较,选出最大数。如果非叶子结点更大,则不用动;如果孩子更大,则交换值,并且交换后有可能影响子树的大顶堆结构,需要递归调整

应用

  • 《剑指offer》求数组中最小的k个数

代码

//函数作用:针对数组的[0,end]范围,对非叶子结点start及其以下的结点构建大顶堆。此时start以下的结点已经满足大顶堆定义。
void buildMaxHeap(vector<int> nums, int start, int end){
    //思路:将当前非叶子结点和它的两个左右孩子比较,选出最大数。
    //如果非叶子结点更大,则不用动;
    //如果孩子更大,则交换值,并且交换后有可能影响子树的大顶堆结构,需要递归调整
    int lchild = 2*start+1;
    int rchild = 2*start+2;
    //默认最大数是当前非叶子结点
    int maxIndex = start;
    //左孩子和最大数比较
    if( lchild<=end && nums[lchild]>nums[maxIndex] ){
        maxIndex = lchild;
    }
    //右孩子和最大数比较
    if( rchild<=end && nums[rchild]>nums[maxIndex] ){
        maxIndex = rchild;
    }
    //判断最大数是否为当前非叶子结点
    if( maxIndex==start ){
        return;
    }else{
        swap(nums[start],nums[maxIndex]);
        buildMaxHeap(nums, maxIndex, end); //交换后有可能影响子树的大顶堆结构,需要递归调整
    }
}
void heapSort(vector<int> nums){
    //1、从最后一个非叶子结点nonleaf=nums.size()/2-1开始,从下往上构建大顶堆
    int size = nums.size();
    for(int nonleaf = size/2-1; nonleaf--; nonleaf>=0){
        buildMaxHeap(nums, nonleaf, size-1);//针对数组的[0,size-1]范围,对非叶子结点nonleaf及其以下的结点构建大顶堆
    }
    //2、将最大元素交换下沉到数组末尾
    for(int i=size-1; i--; i>=0){
        swap(nums[0], nums[i]); //下沉元素
        //交换元素之后,只有堆顶元素不满足大顶堆定义,所以只需要为其构建大顶堆,而无需再从下往上重新构建了
        buildMaxHeap(nums, 0, i-1); //针对数组的[0,i-1]范围,对非叶子结点nonleaf=0及其以下的结点构建大顶堆
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值