文章目录
基础知识
- 比较类排序:通过比较确定元素次序, 时间复杂度不会低于O(nlgn)
- 非比较类排序:不通过比较确定次序,通常是用空间换时间,能够达到O(n)的时间复杂度,也成为线性时间非排序比较。
- in-place / out-place:区别在于是否 使用额外的数组 辅助排序
- 稳定排序:数组中相等的元素在排序后的相对顺序不变
比较类排序(以升序为例)
1. 冒泡排序
- 算法思想:比较相邻元素,如果第一个元素大于第二个元素,则进行交换
- 算法分析:最坏复杂度O(n2), 稳定排序,in-place
- 优化:内循环设置flag, 如果该循环没有发生swap, 说明达到了最优,停止排序
void bubbleSort(vector<int>& nums) {
int len = nums.size();
for(int j = 0; j < len - 1; j++){
bool flag = true;
for(int i = 0; i < len - j -1; i++){
if(nums[i] > nums[i+1]){
swap(nums[i], nums[i+1]);
flag = false;
}
}
if(flag) return ; //说明达到了最优
}
}
但显然,每一次循环都有可能发生多次交换,我们可以记录未排序的数组中的最大值索引,然后在内循环结束后把最大值放入对应位置即可,只需要一次交换。基于这个思想,选择排序被提出。
2. 选择排序
- 基本思想:每次循环选择最大值放入数组尾部
- 分析:不稳定排序,最坏时间复杂度O(n2), in-place
- 为什么不稳定: 比如 7 1 2 5 3 5 8 , 7 和第二个5交换位置,会破坏稳定性。
void selectSort(vector<int>& nums) {
int len = nums.size();
for(int j = 0; j < len - 1; j++){
int max = 0;
//记录最大值的索引
for(int i = 0; i < len - j ; i++){
max = (nums[i] > nums[max]) ? i: max;
}
if(max != len - j -1)
swap(nums[max], nums[len - j - 1]);
}
}
我们把数组看作已排序和未排序两部分,则冒泡和选择都是从未排序部分中选择中最值,然后放入已排序部分的边界位置。同样的,我们也可以直接选择一个未排序元素,找到它在已排序部分中的位置,插入进去。插入排序就是这种思想。
3.插入排序
- 基本思想: 在0 - i-1元素有序的情况下,依次将第i个元素插入前面的有序数组, 同时将大于它的数组元素后移
- 分析:最坏时间O(n2), 稳定排序,in-place
void insertSort0(vector<int>& nums) {
int len = nums.size();
for(int i = 1; i < len ; i++){
if(nums[i] >= nums[i-1]) continue;
int temp = nums[i];
//在0- i-1中查找nums[i]应该插入的位置
int t = 0;
for( t = i-1; t >=0 && temp < nums[t] ; t--){
nums[t+1] = nums[t];
}
nums[t+1] = temp;
}
}
- 优化 : 对于大规模数据,我们也可以使用二分查找来确定要插入的位置。但这种模式并没有优化时间复杂度,因为后续的数组元素向后移动,依然需要O(n)的时间。
void insertSort(vector<int>& nums) {
int len = nums.size();
for(int i = 1; i < len ; i++){
if(nums[i] >= nums[i-1]) continue;
//在0- i-1中查找nums[i]应该插入的位置
//可用二分查找,但总体效率不会变快
int l = 0, r = i - 1;
int mid = 0;
while(l < r){
mid = l + (r - l)/2;
if(nums[mid] <= nums[i]) l = mid + 1;
else if(nums[mid] > nums[i]) r = mid;
}
//插入到nums[l]的位置
//移动元素
int min = nums[i];
for(int t = i; t > l; t--){
nums[t] = nums[t-1];
}
nums[l] = min;
}
}
插入排序在对几乎已经排好序的数据操作时,效率高,最优复杂度为O(n);
但其一般情况下是低效的,所以可以用较低的代价让数组尽量有序,降低调用插入排序的代价。也就是下面的希尔排序。
基于这个原因,我们也可以使用快排+ 插入排序相结合的方式,在快排划分子数组到一定规模,继而使用插排,这也是stl的sort的一个实现思想。
4.希尔排序
- 基本思想:插入排序的优化版本,也成为递减增量排序算法。将间隔为gap的元素视为一组数据,调用插入排序; gap 从len/2 逐渐减半至1,每一个gap执行一次插入排序。
- 分析: 时间复杂度再O(nlgn) ~O(n2)之间,非稳定排序,in-place
void shellSort(vector<int>& nums) {
int len = nums.size();
for(int gap = len /2; gap >= 1; gap /= 2){
//间隔gap 的一次插入排序
//gap = 1时,就相当于普通的插入排序,这时数据基本有序,所以执行会很快
for(int i = gap; i < len ; i++){
int temp = nums[i];
int r = i - gap;
while(r >= 0 && temp < nums[r]){
nums[r + gap] = nums[r];
r -= gap;
}
nums[r + gap] = temp;
}
}
}
5.归并排序
- 基本思想 : 分治算法,递归划分数组直到只有一个元素,然后再逐级合并相邻两个有序子数组
- 分析:最坏时间O(nlgn), 稳定,out-place, 空间复杂度O(n)
//两个有序数组合并 [l, q] [q + 1, r]
void merge(vector<int>& nums, int l, int q, int r){
vector<int> temp;
int index1 = l, index2 = q+1;
while(index1 <= q && index2 <= r){
if(nums[index1] <= nums[index2]){
temp.push_back(nums[index1++]);
}else
temp.push_back(nums[index2++]);
}
while(index1 <= q){
temp.push_back(nums[index1++]);
}
while(index2 <= r){
temp.push_back(nums[index2++]);
}
//数据转移
int index = 0;
for(int i = l; i <= r; i++){
nums[i] = temp[index++];
}
}
//递归框架
void partition(vector<int>& nums, int left, int right){
if(left == right) return ;
int q = left + (right - left)/2;
partition(nums, left, q);
partition(nums, q + 1, right);
merge(nums, left, q, right);
}
void mergeSort(vector<int>& nums) {
partition(nums, 0, nums.size() -1);
}
6.快速排序(重点!)
- 基本思想 : 分治思想,是应用中使用最多的排序。对于一个数组,选择其中一个元素作为主元,根据与主元的关系将数组划分为左右两个区间(左区间<=主元,右区间> 主元),递归划分直到区间元素为0.
- 分析 : 时间O(nlgn), 不稳定排序,in-place
- 注意点: 划分左右区间时,不要把主元再放入子区间,不然可能会发生死循环。
void quitSort(vector<int>& nums, int left, int right) {
if(left >= right) return ;
int key = nums[right];
//划分左右子区间
int p = left ;
for(int i = left; i < right; i++){
if(nums[i] <= key){
swap(nums[i] , nums[p++]);
}
}
cout<<p <<" "<< right<<endl;
swap(nums[p], nums[right]);
quitSort(nums, left, p -1); //注意时p-1!!!
quitSort(nums, p+1, right);
}
- 优化 : 选取最后一个元素作为主元,对于有序数组来说,时间复杂度为O(n2)。 因此我们可以随机选取主元来避免该情况:
int index = p + rand()%(r - p + 1);
//记得换到最后一个上
swap(nums[index], nums[r]);
但是随机选取对于都是重复数字(比如都是2)的数组来说,依然没有优化作用。更近一步,我们可以选择三路快排的方法来优化同一元素很多的情况,即三值取中, 左边是< x, 中间是 == x, 右边是> x 的元素:
//三路快排
pair<int, int> Partition3(vector<int>& nums, int p, int r){
// //随机选取主元
int index = p + rand()%(r - p + 1);
swap(nums[index], nums[r]);
//三路
int i = p -1;
int k = r ;
int x = nums[r];
for(int j = p; j < r && j < k ; j++){
if(nums[j] < x){
i++;
swap(nums[i], nums[j]);
}else if(nums[j] > x){
k--;
swap(nums[k], nums[j]);
j--; //注意这个,比如 5 1 1 2 0 0, 可能交换后,nums[j]依然是大于x的
}
}
swap(nums[k], nums[r]);//与nums[k]交换
return make_pair(i, k + 1);
}
void QSort(vector<int> & nums, int left, int right){
if(left >= right)
return ;
pair<int,int> q = Partition3(nums, left, right);
QSort(nums, left, q.first);
QSort(nums, q.second, right);
}
- 那么c++ stl里面的sort 是怎么实现的呢?
简单来说,先调用快排进行分段,如果该段区间元素小于一定阈值(16),用插排,如果递归深度达到一定阈值(2*lg(n)),停止继续递归,当前子区间用堆排序。
sort具体源码分析
- 快排的复杂度为什么是O(nlgn)?
ref:https://harttle.land/2015/09/27/quick-sort.html
7. 堆排序
- 基本思想 : 通过构建大根堆,每次输出一个最大值,循环n遍
-
- 分析 : 时间O(nlgn), 不稳定,in-place
//建立大根堆, 从下标0开始
void heapBuild(vector<int> &nums){
int len = nums.size();
//从底向上
for(int i = (len-1)/2; i >= 0; i--)
maxHeapify(nums, i, len);
}
//维护以i为根, 长度为n的大根堆性质(可原地排序)
void maxHeapify(vector<int> &nums, int i, int len ){
int left = 2 * i + 1;
int right = 2 * i + 2;
int large = i;
if(left < len && nums[left] > nums[i]){
swap(nums[left], nums[i]);
large = left;
}
if(right < len && nums[right] > nums[i]){
swap(nums[right], nums[i]);
large = right;
}
//注意判断large != i, 否则会死循环
if(large != i){
maxHeapify(nums, large, len);
}
}
void heapSort(vector<int>& nums){
heapBuild(nums);
int len = nums.size() ;
while(len > 0){
swap(nums[0], nums[len - 1]);
len--;
maxHeapify(nums, 0, len);
}
}
基于比较的排序如上,最好的时间复杂度是O(nlgn), 无法到达线性时间,而下面的非比较类排序,则可以突破这个问题。
非比较类排序(以升序为例)
8.计数排序
- 基本思想 : 创建数组,统计nums中各数值出现次数,然后依次将元素复制到nums中的对应位置
- 分析 : 时间O(n + k), 稳定,out-place, 空间O(k+n), k为数组取值范围
- 优化 : 用map计数会节省一些空间O(n)
void countSort(vector<int>& nums){
int len = nums.size() ;
map<int,int> mp;
for(int num : nums){
mp[num]++;
}
int i = 0;
map<int, int>::const_iterator it = mp.begin();
for(; it != mp.end(); it++){
int key = it->first;
int count = mp[key];
while(count--){
nums[i++] = key;
}
}
}
9.桶排序
- 基本思想 : 将原数组元素分到有限数量的桶中,然后分别对每一个桶排序
- 分析 : 时间O(n + k), 稳定,out-place, 空间O(k+n), k为数组取值范围
- 注意 : 桶的个数必须大于等于1, 所以需要+1
void bucketSort(vector<int>& nums){
int n = nums.size();
// 获取数组的最小值和最大值
int maxNum = nums[0], minNum = nums[0];
for (int i = 1; i < n; ++i) {
if (nums[i] > maxNum) maxNum = nums[i];
if (nums[i] < minNum) minNum = nums[i];
}
//初始化桶
int bucketNum = 5, bucketSize = (maxNum - minNum)/bucketNum + 1; //最少有一个桶!!
vector<vector<int>> buckets(bucketNum);
for(int num : nums){
int index = (num - minNum)/bucketSize;
buckets[index].push_back(num);
}
//对每一个桶排序
for(int i = 0; i < bucketNum; i++){
sort(buckets[i].begin(), buckets[i].end());
}
//写入nums
int j = 0;
for(int i = 0; i < bucketNum; i++){
for(int num : buckets[i]){
nums[j++] = num;
}
}
}
10.基数排序
- 基本思想 : 按照位数,从低到高依次排序,每一次都是基于上一次的排序结果,所以保持稳定
- 分析 : 时间O(nm), 稳定,out-place, 空间O(k+n), m为数值的最大位数
- 注意 : 要求数值是非负整数,所以如果有负数,可以加上一个值,进行数据处理。 以及如何按照某一位的数据进行排序的方法如何实现(计数排序)。
//按照某一位排序(计数排序)
void radix(vector<int>& nums, vector<int>& temp, int divisor){
vector<int> count(10, 0);
int n = nums.size();
for(int num : nums){
//取得该位上的数字
int x = (num/divisor)%10;
if(x != 9) count[x + 1]++;
}
//计算前缀和, count[i]代表i对应的起始位置
for(int i = 1; i < 10; i++){
count[i] += count[i-1];
}
for(int num : nums){
int x = (num/divisor)%10;
temp[count[x]] = num;
count[x]++;
}
}
void radixSort(vector<int>& nums){
int n = nums.size();
// 预处理,让所有的数都大于等于0
for (int i = 0; i < n; ++i) {
nums[i] += 50000; // 50000为最小可能的数组大小
}
// 找出最大的数字,并获得其最大位数
int maxNum = nums[0];
for (int i = 0; i < n; ++i) {
if (nums[i] > maxNum) {
maxNum = nums[i];
}
}
int num = maxNum, maxLen = 0;
//求最大位数
while(num){
maxLen++;
num /= 10;
}
vector<int> temp(n, 0);
//从低位到高位排序
int divisor = 1;
for(int i = 0; i < maxLen; ++i){
radix(nums, temp, divisor);
swap(temp, nums);
divisor *= 10;
}
//减去与处理量
for (int i = 0; i < n; ++i) {
nums[i] -= 50000; // 50000为最小可能的数组大小
}
}
汇总
图片来源下面的参考链接1.
参考链接
- https://leetcode.cn/problems/sort-an-array/solution/by-peaceful-thompsonfsu-b3bu/