排序(冒泡、插排、选择、桶排、希尔、堆、归并、快排) C++
前提说明:
排序可以是升序,可以是降序,可以从后往前排,可以从前往后排,结果正确即可,本文以升序为主。
随机数组生成
void randomArr(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
arr[i] = i;
}
// 如果你要产生0~99这100个整数中的一个随机整数,可以表达为:int num = rand() % 100;
for (int i = 0; i < n; i++)
{
std::swap(arr[i], arr[rand()%n]);
}
}
辅助打印函数(数组)
void printArr(int arr[], int n)
{
for(int i=0; i<n; i++)
{
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
辅助打印函数(vector)// 重载
void printArr(std::vector<int> vec)
{
for(int i=0; i<vec.size(); i++)
{
std::cout << vec[i] << " ";
}
std::cout << std::endl;
}
一、冒泡
思想:两两比较,顺序不交换,逆序前后交换
- 冒泡冒泡,十分的形象的描述,每一次都将最大的数,吹到待排序的最后一个位置(或者将最小的数放在第一个位置);
- n 个数最多 n-1 趟冒泡;
- 每一趟,前后两数比较,大的去后面,小的去前面;
- i 控制排序趟数,j 控制比较的两个数;
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N) | O(N^2) | O(N^2) | O(1) | 稳定 |
void bubbleSort(int arr[], int n)// 数组传的就是引用,n 是长度
{
for(int i=0; i<n-1; i++)
{
for(int j=0; j<n-i-1; j++)
{
if(arr[j] > arr[j+1])
{
std::swap(arr[j], arr[j+1]);
}
}
}
}
冒泡plus
设置一个 flag ,当某一趟没有交换的情况下,排序完成
void bubbleSortPlus(int arr[], int n)// 数组传的就是引用,n 是长度
{
for(int i=0; i<n-1; i++)
{
bool flag = true;
for(int j=0; j<n-i-1; j++)
{
if(arr[j] > arr[j+1])
{
std::swap(arr[j], arr[j+1]);
flag = false;
}
}
if(flag)
{
break ;
}
}
}
二、直接插入
思想:
- 数组分为,有序部分和待排序部分,开始有序部分为第一个元素,第二个往后都是待排序部分;
- 从第二个元素开始;
- 先记录自己的 val ;
- 和前面的元素依次比较,直到找到 自己小的小的元素,或到达队首;
- 如果比自己大,就往后顺一个位置;
注:最多插入次数,n - 1次;
在基本有序和数据量少时,直接插入排序是比较好的选择,效率非常高
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N) | O(N^2) | O(N^2) | O(1) | 稳定 |
void insertSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
int j = i + 1;
int tmp = arr[j];
while (j > 0)
{
// 要用 tmp 去比较,在每趟比较中tmp才是要插入的值,且他不变
if (tmp >= arr[j - 1])
{
break;
}
arr[j] = arr[j - 1];
j--;
}
arr[j] = tmp;
}
}
简化一下内部循环代码:
void insertSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
int j = i + 1;
int tmp = arr[j];
// 修改循环结束条件
while (j > 0 && tmp < arr[j - 1])
{
arr[j] = arr[j - 1];
j--;
}
arr[j] = tmp;
}
}
三、希尔排序
直接插入排序的 plus
思想:
- n 个数据,分成 gap = n / 2 组数据,对每一组数据进行插入排序;
- 缩进 1 / 2,即 gap 组数据,变成 gap /2组数据,在进行插排;
- 重复操作 2 ,直到 gap == 1
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
void shellInsert(int arr[], int start, int gap)
{
int tmp = arr[start];
while (start >= gap && tmp < arr[start - gap])
{
arr[start] = arr[start - gap];
start -= gap;
}
arr[start] = tmp;
}
void shellSort(int arr[], int n)
{
// 控制分组
for(int gap = n/2; gap > 0; gap /= 2)
{
// 从start为下标开始的元素,依次插入到合适的位置
int start = gap;
while (start < n)
{
shellInsert(arr, start, gap);
start++;
}
}
}
四、选择排序
思想:
- 从前往后,最多进行 n-1 轮排序;
- 每一轮以待排序数组的一个元素值为 min,minIndex记录最小值下标,每次开始为待排序数组的第一个元素的下标,与待排序的其他值进行比较,比 min 小则,maxIndex 修改;
- min也要修改 每一轮遍历完,比较 minIndex 和 待排序数组的第一个元素的下标是否相同,相同,不交换,不相同,则交换,进入下一轮交换;
注:
或者紧记录 minIndex 的值,并每次修改,比较时,比较 当前元素和 arr[minIndex] 的值的大小。
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
void selectSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
int min = arr[i];
int minIndex = i;
for (int j = i + 1; j < n; j++)
{
if (arr[j] < min)
{
// 不仅要修改 minIndex 的值,还要修改 min 的大小
min = arr[j];
minIndex = j;
}
}
if (i != minIndex)
{
std::swap(arr[i], arr[minIndex]);
}
}
}
简化版:
void selectSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < n; j++)
{
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
if (i != minIndex)
{
std::swap(arr[i], arr[minIndex]);
}
}
}
五、堆排序
由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
堆:
性质一:索引为i的左孩子的索引是 (2i+1);
性质二:索引为i的右孩子的索引是 (2i+2);
性质三:索引为i的父结点的索引是 (i-1)/2;
大顶堆:堆中,父节点的值比任一孩子节点的值都大。
小顶堆:堆中,父节点的值比任一孩子节点的值都小。
思想:
选择排序plus
- 初始化大顶堆(或小顶堆);
- 将堆顶元素和待排序队列最后一个元素交换,队列长度 -1 ,重新调整为大顶堆;
- 直到剩下根节点为止;
void heapSort(int arr[], int n)
{
for(int i = n/2-1; i >= 0; i--)
{
heapAdjust(arr, i, n);
}
for(int i = n-1; i > 0; i--)
{
std::swap(arr[0], arr[i]);
heapAdjust(arr, 0, i);
}
}
构建大顶堆:
- 从 n / 2,开始,到根节点为止;
- 每一次比较父节点和子节点中大的那个节点,小于,则将父节点的位置赋值为子节点大的值,否则结束循环;
- 如发生赋值,则继续判断是否子节点部分的大顶堆被破坏,找到父节点值的合适位置。
/*
*第二个参数,起始父节点,第三个参数数组长度
*/
void heapAdjust(int arr[], int father, int n)
{
int fVal = arr[father];
for(int i = 2*father+1; i < n; i = 2*father+1)
{
if(i<n-1 && arr[i]<=arr[i+1])
{
i++;
}
if(fVal >= arr[i])
{
break;
}
arr[father] = arr[i];
// 不能少,否则陷入死循环
father = i;
}
arr[father] = fVal;
}
注:
// 不能少,否则陷入死循环
father = i;
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(NlogN) | O(NlogN) | O(NlogN) | O(1) | 不稳定 |
六、桶排序(基数排序的特殊)
适用于元素比较紧密的自然数
思想:
- 以队列每个元素的 val 作为桶的序号;
- 先找到数组的最大值 max,然后生成 max+1 个桶;
- 遍历数组,将元素放入桶中,从 0开始,有就++;
- 遍历完数组后,遍历桶,给原数组赋值,该桶不为空,将下标赋值给原数组,val–,直到桶遍历完;
int getMAX(int arr[], int n)
{
int max = 0;
for(int i=1; i<n; i++)
{
if(arr[max] < arr[i])
{
max = i;
}
}
return arr[max];
}
void bucketSort(int arr[], int n)
{
// 错误用法,int[] 数组里面必须是常数
// int bucket[getMAX(arr, n)+1]{0};
// int* bucket = new int[getMAX(arr, n) + 1]{0};
int* bucket = new int[getMAX(arr, n) + 1];
memset(bucket, 0, sizeof(int)*(getMAX(arr, n) + 1));
// vector 不用delete
// std::vector<int> bucket(getMAX(arr, n)+1, 0);
for(int i=0; i<n; i++)
{
bucket[arr[i]]++;
}
int i = 0;
int j = 0;
while(j < n)
{
while(bucket[i]--)
{
arr[j++] = i;
}
i++;
}
delete [] bucket;
bucket = NULL;
}
学习笔记:
{0} 只能在初始化是使用;
memset 可以在任何时候使用;
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N) | O(N*M) | O(N^M) | O(M) | 稳定 |
注:N为数据个数,M为数据位数
七、归并排序
归并排序是一种占用内存,但却效率高且稳定的算法。
思想:
- 将待排序队列进行分解,分解成若干有序队列(单个元素必为有序队列);
- 在将每部分的有序队列,两两合并;
- 最终重新合并成一个队列;
递归实现(合并的代码在下面)
/*
*递归
*业务代码主要实现函数
*/
void mergeSort(int arr[], int left, int right)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并
merge(arr, left, mid, right);
}
/*
*主调函数
*/
void mergeSort(int arr[], int n)
{
// 右边界索引是 n-1
mergeSort(arr, 0, n-1);
}
非递归实现:
/*
*非递归主要业务逻辑
*/
void mergeSortWhile(int arr[], int len, int n)
{
int i = 0;
int mid = i + len - 1;
int right = i + len * 2 - 1;
while (right < n)
{
merge(arr, i, mid, right);
// printArr(arr, n);
i = right+1;
mid = i + len - 1;
right = i + len * 2 - 1;
}
if (mid < n)
{
merge(arr, i, mid, n - 1);
}
// printArr(arr, n);
}
/*
* 非递归主调函数
*/
void mergeSortWhile(int arr[], int n)
{
// len 是每次合并的单个队列长度
int len = 1;
while (len < n)
{
mergeSortWhile(arr, len, n);
len *= 2;
}
}
合并的思路:
- 申请待合并队列之和的空间;
- 比较两个队列的大小,将值放进去,直到其中一个队列走到尽头;
- 处理尾部,将未走到尽头的队列的剩余元素,放入申请的新内存中;
void merge(int arr[], int left, int mid, int right)
{
int* tmp = (int*)malloc(sizeof(int)*(right - left + 1));
int l, index, r;
// index 从0开始,不然可能会越界 l [left, mid], r [mid+1, right]
index = 0; l = left; r = mid+1;
// 两个待合并队列,有一个到达队尾就跳出循环
while(l <= mid && r <= right)
{
// 赋值给 tmp
tmp[index++] = arr[l] <= arr[r] ? arr[l++] : arr[r++];
}
while(l <= mid)
{
tmp[index++] = arr[l++];
}
while(r <= right)
{
tmp[index++] = arr[r++];
}
// 记得重新给队列赋值
l = left;
while(l <= right)
{
arr[l] = tmp[l - left];
l++;
}
// 释放内存
delete [] tmp;
}
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N*logN) | O(N*logN) | O(N*logN) | O(N+logN) | 稳定 |
注:非递归归并的辅助空间,降低到 O(N),因为少了栈空间的迭代。
八、快速排序
前提:三色旗问题
一个数组中,仅有0、1、2三个元素,对他们进行排序。(很显然,我们可以用桶排序,但是,我们不那么做)
思路:
- 以 1 为基准,将小于 1 的部分放到左边;
- 大于 1 的放到右边;
- 等于 1 时,继续遍历下一个元素即可;
/*
*三色旗,荷兰国旗问题
*/
void holandFlag(int arr[], int n)
{
int l, i, r;
l = -1; i = 0; r = n;
while(i < n && i < r)
{
if(arr[i] < 1)
{
std::swap(arr[++l], arr[i++]);
}
else if(arr[i] == 1)
{
i++;
}
else
{
std::swap(arr[i], arr[--r]);
}
}
}
/*
*主调函数
*/
int main()
{
int arr[10]{1, 2, 0, 0, 1, 2, 2, 0, 0, 1};
int n = sizeof(arr) / sizeof(arr[0]);
holandFlag(arr, n);
// 打印方法在片头
printArr(arr, n);
return 0;
}
快排思路:
- 选取一个基准值,对队列进行划分,分成小于基准值部分的第一部分,等于基准值部分的第二部分和大于基准值的第三部分;
- 对第一部分和第三部分,重复 1 的步骤;
- 直到第一部分和第三部分只剩下一个元素或者剩下元素都相同为止;
递归实现1(元素不重复时使用):
/*
*用于划分的方法
*left 为左边界索引
*right 为右边界索引
*/
int quickFlag(int arr[], int left, int right)
{
// 枢轴,枢轴的选取也是一个难点,简便起见,我们这里直接采用左边界为枢轴
int pivotkey = arr[left];
while (left < right)
{
while (left < right && arr[right] > pivotkey)
{
right--;
}
std::swap(arr[left], arr[right]);
while (left < right && arr[left] < pivotkey)
{
left++;
}
std::swap(arr[left], arr[right]);
}
return left;
}
/*
*快排递归函数
*/
void quickSort(int arr[], int left, int right)
{
// 递归 end condition
if(left >= right)
{
return ;
}
int pivot = quickFlag(arr, left, right);
quickSort(arr, left, pivot-1);
quickSort(arr, pivot+1, right);
}
/*
*快排递归主函数
*/
void quickSort(int arr[], int n)
{
quickSort(arr, 0, n-1);
}
递归实现2
划分:
/*
*用于划分的方法
*left 为左边界索引
*right 为右边界索引
*/
std::pair<int, int> quickFlag(int arr[], int left, int right)
{
int l, i, r;
// 枢轴,枢轴的选取也是一个难点,简便起见,我们这里直接采用左边界为枢轴
int pivotkey = arr[left];
l = left-1; i = left; r = right+1;
while(i < right+1 && i < r)
{
if(arr[i] < pivotkey)
{
std::swap(arr[++l], arr[i++]);
}
else if(arr[i] == pivotkey)
{
i++;
}
else
{
std::swap(arr[i], arr[--r]);
}
}
return std::make_pair(l, r);
}
主逻辑:
/*
*快排递归函数
*/
void quickSort(int arr[], int left, int right)
{
// 递归 end condition
if(left >= right)
{
return ;
}
std::pair<int, int> pivot = quickFlag(arr, left, right);
quickSort(arr, left, pivot.first);
quickSort(arr, pivot.second, right);
}
/*
*快排递归主函数
*/
void quickSort(int arr[], int n)
{
quickSort(arr, 0, n-1);
}
非递归实现(栈):
void quickSortStack(int arr[], int n)
{
std::stack<std::pair<int, int>> stk;
std::pair<int, int> pivot(0, n-1);
stk.push(pivot);
// 循环条件,栈不为空
while(!stk.empty())
{
pivot = stk.top();
stk.pop();
std::pair<int, int> newPivot = quickFlag(arr, pivot.first, pivot.second);
if(pivot.first < newPivot.first)
{
stk.push(std::make_pair(pivot.first, newPivot.first));
}
if(newPivot.second < pivot.second)
{
stk.push(std::make_pair(newPivot.second, pivot.second));
}
}
}
时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|
最好 | 平均 | 最坏 | 辅助空间 | 每次排序后,是否会改变相同数的次序。 |
O(N*logN) | O(N*logN) | O(N^2) | O(logN)~O(N) | 不稳定 |
本篇全部主调函数:
#include "SortReview.h" // 作者创建的头文件
int main()
{
SortReview s;
int arr[10];
int n = sizeof(arr) / sizeof(arr[0]);
std::cout << "Bubble Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.bubbleSort(arr, n);
s.printArr(arr, n);
std::cout << "Bubble Plus Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.bubbleSortPlus(arr, n);
s.printArr(arr, n);
std::cout << "Insert Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.insertSort(arr, n);
s.printArr(arr, n);
std::cout << "Shell Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.shellSort(arr, n);
s.printArr(arr, n);
std::cout << "Select Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.selectSort(arr, n);
s.printArr(arr, n);
std::cout << "Heap Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.heapSort(arr, n);
s.printArr(arr, n);
std::cout << "Bucket Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.bucketSort(arr, n);
s.printArr(arr, n);
std::cout << "Merge Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.mergeSort(arr, n);
s.printArr(arr, n);
std::cout << "MergeWhile Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.mergeSortWhile(arr, n);
s.printArr(arr, n);
std::cout << "Quick Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.quickSort(arr, n);
s.printArr(arr, n);
std::cout << "Quick Stack Sort : " << std::endl;
s.randomArr(arr, n);
s.printArr(arr, n);
s.quickSortStack(arr, n);
s.printArr(arr, n);
}
九、排序总结:
分类:
简单算法:冒泡、直接插排、简单选排、桶排
改进算法:shell(希尔排序——直接插排plus)
堆排(选择排序plus)
归并排序
快排(冒泡排序plus)
交换排序类 | 插入排序类 | 选择排序类 | 归并类 | 基数类 |
---|---|---|---|---|
冒泡 快排 | 插排 shell | 选排 堆排 | 归并 | 桶排 |
注:标红的是笔者认为比较重要的,当然这只是一个相对性。
学习笔记:
- 数据量少或基本有序时,不考虑复杂的改进算法;
- 对内存要求比较高时,归并和排序不是好选择;
- 如果内存无要求,归并绝对是强者;
注:当然,他们的优劣势还有很多,读者可以根据需要总结并应用。
2020/06/28 13:46
@luxurylu
点个赞鼓励下笔者~
如有错误欢迎指出,感谢~