快速排序
快排思想
不断的选定一个基准(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及其以下的结点构建大顶堆
}
}