目录
哈喽,大家好😄
哈喽,今天我来总结一下C++中的8中排序方法,这些排序在实际开发中能起到一些作用,也可以锻炼你的算法头脑。我也好长时间没上热榜了,这次准备做一个非常详细🔎的总结,看看能不能上热榜。
排序算法
既然叫排序算法,肯定要排序。
我们要实现的效果是输入几个数,
输出升序(或降序)排序后的结果,
并找到时间复杂度最低的算法,应用到实际开发中。
冒泡排序
冒泡排序主要思路就是遍历数组,比较两个相邻的元素,也就是 arr[j] 和 arr[j+1] ,如果顺序错误就用 swap 函数或空杯交换,交换这两个元素。
举个例子,数组是 a[5]={2,1,3,4,0};,我们要从小到大排序。
先将2和1进行比较,不是正序,相互交换位置,序列变为{1,2,3,4,0}。
再将2和3进行比较,序列为{1,2,3,4,0}。
再将3和4进行比较,序列为{1,2,3,4,0}。
再将4和0进行比较,不是正序,相互交换顺序,序列变为{1,2,3,0,4}。
至此,第1轮冒泡就已经完成了,最大值4到了序列的最后面。
由于4的位置已经排好序,所以第4轮,5不再参与排序,将{1,2,3,0}置为有序序列即可。
假设数组元素个数为n,从上面第1轮的比较来看,我们可以得出如下结论:
我们将冒泡排序的轮数设为 i ,每完成1轮冒泡排序,就会增加一个元素处于有序状态,所以在 (n-1) 轮排序结束后,就会有 n-1 个元素处于有序状态,而剩下的最后一个元素,自然是最小(大)值,不用再进行排序,所以,冒泡排序比较的轮数为 (n-1) 。
我们将每轮需要比较的次数设为 j ,第1轮( i 值为0)需要比较的次数为4,从 {3,4,1,0} 中不难看出,第2轮( i 值为1)需要比较的次数为3次,说明每轮比较的次数 j 与冒泡的轮数 i 值有关,且 j = n-i-1。
确定好上面两条结论以后,我们开始用代码实现冒泡排序算法:
#include <iostream>
using namespace std;
//对a[]进行正序(从小到大)排序
void bubblesort(int *a,int len) //形参a取到实参a传递过来的数组首地址
//然后解引用,取到数组的值
{
for (int i=0;i<len-1;i++) //i控制排序的轮数
{
for (int j=0;j<len-i-1;j++) //j控制每轮需要比较的次数
{
if(a[j+1]<a[j]) //不满足正序要求,交换顺序
{
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
}
int main()
{
int a[10]={2,6,3,8,5,1,0,7,9,4};
int len = sizeof(a)/sizeof(int); // 计算数组元素个数
bubblesort(a,len); //a为数组a[10]首地址,作为实参传递给形参
for(int i=0;i<len;i++)
{
cout<<a[i]<<" ";
}
return 0;
}
计数排序
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
原理(图片来源于网络,原地址找不到了,肯定是手绘的)
统计关键字的个数,放入数组C
对C进行累加处理,为了找对应A的排好序的位置
详细操作过程
代码
#include <iostream>
using namespace std;
//计数排序(不涉及比较动作)
void CountSort(int A[], int n) {
//临时数组,存放结果
int *R = new int(n);
//为了方便我们是0~5的范围,真实情况需要找到数组的跨度
int C[6];
//对应第一张图
for (int i = 0; i < n; ++i) {
C[A[i]]++;
}
//对应第二张图
for (int i = 1; i < 6; ++i) {
C[i] = C[i - 1] + C[i];
}
//对应第三张图
for (int i = n - 1; i >= 0; --i) {
R[C[A[i]] - 1] = A[i];
--C[A[i]];
}
//将R拷贝给A
for (int i = 0; i < n; ++i) {
A[i] = R[i];
}
}
//打印输出
void ArrPrint(int A[], int n) {
for (int i = 0; i < n; ++i) {
cout << A[i] << " ";
}
}
int main() {
//待排序列
int A[8] = {2, 5, 3, 0, 2, 3, 0, 3};
//排序调用
CountSort(A, 8);
//打印输出
ArrPrint(A, 8);
return 0;
}
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
#include<iostream>
using namespace std;
void select_sort(int* a,int n)
{
for (int i = 0; i < n; i++)
{
int index = i;
for (int j = i + 1; j < n; j++)
{
if (a[index] > a[j])index = j;
}
swap(a[i], a[index]);
}
}
int main()
{
int a[10]{ 5,7,9,6,3,1,4,8 };
select_sort(a, 8);
for (int i = 0; i < 8; i++)
{
cout << a[i]<<" ";
}
return 0;
}
快速排序
每次都取数组的第一个元素作为基准元素,凡是大于这个基准元素的都放在他的右边,凡是小于这个基准元素的都放在它的左边,具体步骤如下:
1.设置两个变量i和j(也称为哨兵),令序列第一个元素作为基准元素
2.i指向序列的最左边,j指向序列的最右边,j从右往左试探,i从左往右试探,直到j找到小于基准的数就停止,i找到大于基准的数就停止,交换i和j指向的两个数,j继续往左试探,i继续往右试探
3.如果i和j相遇,则i或j上的元素与基准元素交换,则这一轮排序结束
4.对基准元素两边的序列重复以上操作
这里以数组 6 2 7 3 9 8 为例,我们的基准元素取首元素6,首先i和j分别为头和尾元素。根据步骤2中,首先j往左边移动直到遇到第一个比6小的元素
此时的j到3这儿,然后i开始往右边移动直到找到第一个比基准元素大的元素,此时找到了7
然后交换i和j指向的两个数据
然后j接着往左边移动再找到第一个小于基准的数据,但是这儿i和j在3数字这儿相遇,由此根据步骤3,把i或j(因为此时的i和j都指向同一个数字3)与基准元素进行交换,这样就完成了第一轮的交换。
此时会发现基准元素6的左边都是比6小,右边都是比6 大,由此第一轮排序结束,然后分别接着对 3 2 这两个数字重复上面的操作(即将3作为基准元素,然后3 和2分别为头和尾元素),7 8 9 也重复上面的操作,最后就可以完成整个数组的排序
#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
using std::swap;
void quickSort(vector<int>& vi, int lo, int hi)
{
int pivot = vi[lo];
int i = lo;
int j = hi;
if (lo < hi)
{
while (i != j)
{
while (vi[j] >= pivot && j > i)
{
j--;
}
while (vi[i] <= pivot && j > i)
{
i++;
}
if(i<j)
{
swap(vi[i], vi[j]);
}
}
swap(vi[lo], vi[i]);
quickSort(vi, lo, i-1);
quickSort(vi, i+1, hi);
}
}
int main()
{
vector<int> s{6, 2, 7, 3, 9, 8};
quickSort(s, 0, 5);
for(auto x:s)
{
cout<<x<<endl;
}
return 0;
}
希尔排序
希尔排序是对插入排序的改进
他减少了交换的次数,优化了插入排序。希尔排序算法的基本思想是将待排序的表按照间隔切割成若干个子表,然后对这些之表进行 插入排序。 一般来说 第一次的间隔(divide ) 为整个排序表的一半(divide = ceil(size /2);) 然后对按照这些间隔划分的子表进行直接插入排序,第二次间隔又是第一次的一半( divide = ceil(divide / 2))然后对按照这些间隔划分的子表
#include <iostream>
#include <cmath>
using namespace std;
void shellSort(int arr[],int size) {
int temp,j,i;
int divide = ceil(size /2);
for (divide;divide >= 1; divide = ceil(divide / 2)) {
for (i = divide;i < size;i++) {
if (arr[i] < arr[i - divide]) {
temp = arr[i];
for (j = i - divide;j >=0 && arr[j] > temp;j=j-divide) {
arr[j + divide] = arr[j];
}
arr[j + divide] = temp;
}
}
}
}
int main() {
int test[10] = {38,55,-10,49,67,78,65,38,77,99};
shellSort(test, 10);
unsigned int i = 0;
for (i = 0;i < 10;i++) {
cout << test[i] << " ";
}
return 0;
}
桶排序
桶排序是计数排序的升级版,也是分治算法。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。简言之,将值为i的元素放入i号桶,最后依次把桶里的元素倒出来。
怎么样,是不是很“简单”?
还有这张一看就头疼的图
🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔🤔
桶排序的基本思想是假设数据在[min,max]之间均匀分布,其中min、max分别指数据中的最小值和最大值。那么将区间[min,max]等分成n份,这n个区间便称为n个桶。将数据加入对应的桶中,然后每个桶内单独排序。由于桶之间有大小关系,因此可以从大到小(或从小到大)将桶中元素放入到数组中。
简单说,你有一个数组1 , 3 , 7 , 77 ,100 ,234
比如,
你把一位数、两位数和三位数分到3个桶里,
各自排完序再合到一起
排序前:1 , 3 , 7 , 77 ,100 ,234
一位数:1、7、3
两位数:77
三位数:100、234
排序后:
一位数:1、3、7
两位数:77
三位数:100、234
合起来:1、3、7、77、100、234
思路
1.设置一个定量的数组当作空桶子。
2.寻访序列,并且把项目一个一个放到对应的桶子去。
3.对每个非空的桶子进行排序。
4.从不是空的桶子里把项目再放回原来的序列中。
确定“分桶”个数🔍
假如要对数组arr={ 2,0,1,6,8,10,5,99,87,333,2,0,1 }排序,假设需要桶的个数为bucketNum=std::ceil(size/3),向上取整,反之桶个数不够映射时越界。
复杂度分析😐
桶排序实际上只需要遍历一遍所有的待排序元素,然后依次放入指定的位置,如果加上输出排序的时间,那么需要遍历所有的桶,时间复杂度为O(n+m),其中n为待排序元素的个数,m为桶的个数,这时相当快的排序算法,但是,对于空间的小号来说太大了。当n越大,空间浪费就越大,所以,如果数据跨度过大,桶排序并不适用跨度范围大的排序。
#include <bits/stdc++.h>
using namespace std;
// 打印数组
void print_array(int *arr, int n) {
if(n==0){
printf("ERROR: Array length is ZERO\n");
return;
}
printf("%d", arr[0]);
for (int i=1; i<n; i++)
printf(" %d", arr[i]);
printf("\n");
}
int* sort_array(int *arr, int n) {
int i;
int maxValue = arr[0];
for (i = 1; i < n; i++)
if (arr[i] > maxValue) // 输入数据的最大值
maxValue = arr[i];
// 设置10个桶,依次0,1,,,9
const int bucketCnt = 10;
vector<int> buckets[bucketCnt];
// 桶的大小bucketSize根据数组最大值确定:比如最大值99, 桶大小10
// 最大值999,桶大小100
// 根据最高位数字映射到相应的桶,映射函数为 arr[i]/bucketSize
int bucketSize = 1;
while (maxValue) { //求最大尺寸
maxValue /= 10;
bucketSize *= 10;
}
bucketSize /= 10; //桶的个数
// 入桶
for (int i=0; i<n; i++) {
int idx = arr[i]/bucketSize; //放入对应的桶
buckets[idx].push_back(arr[i]);
// 对该桶使用插入排序(因为数据过少,插入排序即可),维持该桶的有序性
for (int j=int(buckets[idx].size())-1; j>0; j--) {
if (buckets[idx][j]<buckets[idx][j-1]) {
swap(buckets[idx][j], buckets[idx][j-1]);
}
}
}
// 顺序访问桶,得到有序数组
for (int i=0, k=0; i<bucketCnt; i++) {
for (int j=0; j<int(buckets[i].size()); j++) {
arr[k++] = buckets[i][j];
}
}
return arr;
}
int main() {
int n;
scanf("%d", &n);
int *arr;
arr = (int*)malloc(sizeof(int)*n);
for (int i=0; i<n; i++) scanf("%d", &arr[i]);
arr = sort_array(arr, n);
print_array(arr, n);
system("pause");
return 0;
}
归并排序(分治算法)
所谓分治,就是分开治理,把大问题化成小问题,逐个解决,再合到一起
这也就是归并排序的精髓
这种算法时间复杂度低,原理也比较简单
把一个数组分成了一个一个的元素,在合并的过程中排序
怎么分?
分的方法其实很简单,一个递归就可以解决
如果你是初学者,可能没有完全把递归学透彻
简单说,递归就是在函数内部调用自己的函数
递归都要有一个出口,否则就会变成死循环
递归的出口是啥
我们在函数参数上写
1)一个数组(要被排序的数组)
2)分的开始和结束(first和end)
如果first<end,那么我们可以继续递归,如果不满足条件,递归结束
还要定义一个中间,前面那行代码是分左边,也就是开始~中间,后面那行代码是分右边,也就是中间+1~末尾
void merge_sort(int array[],int first,int end)
{
if(first < end){
int center = (first + end)/2; //得到中间数
merge_sort(array,first,center);
merge_sort(array,center+1,end);
}
}
“并”的实现
按照上面的图片,我们每排一下序就给它并一下
具体代码实现
void merge(int array[],int first,int center,int end)
{
int n1 = center - first + 1;
int n2 = end - center;
int L[n1+1];
int R[n2+1];
for(int i = 0; i < n1; i++ )
{
L[i] = array[first+i]; //得到前面一部分数组
}
//printArray(L,n1);
for(int j = 0; j < n2; j++ )
{
R[j] = array[center+j+1]; //得到后面一部分数组
}
//printArray(R,n2);
L[n1] = 1000; //设置哨兵
R[n2] = 1000; //设置哨兵
//cout << "R[5] =" << R[4] << endl;
int k1 = 0;
int k2 = 0;
for (int k = first; k <= end; ++k) //把得到的两个数组进行排序合并
{
//cout << L[k1] <<endl;
//cout << R[k2] <<endl;
if(L[k1] <= R[k2])
{
//cout << L[k1] <<endl;
array[k] = L[k1];
//cout << array[k] << endl;
//cout << "k1 =" << k1 << endl;
k1 = k1 + 1;
}else{
//cout << R[k2] <<endl;
array[k] = R[k2];
//cout << array[k] << endl;
//cout << "k2 =" << k2 << endl;
k2 = k2 + 1;
}
//cout << array[k] <<endl;
}
//printArray(array,10);
}
代码
#include <iostream>
using namespace std;
/*
* 打印数组
*/
void printArray(int array[],int length)
{
for (int i = 0; i < length; ++i)
{
cout << array[i] << endl;
}
}
/*
* 一个数组从中间分成两个有序数组
* 把这两个有序数组合并成一个有序数组
*/
void merge(int array[],int first,int center,int end)
{
int n1 = center - first + 1;
int n2 = end - center;
int L[n1+1];
int R[n2+1];
for(int i = 0; i < n1; i++ )
{
L[i] = array[first+i]; //得到前面一部分数组
}
//printArray(L,n1);
for(int j = 0; j < n2; j++ )
{
R[j] = array[center+j+1]; //得到后面一部分数组
}
//printArray(R,n2);
L[n1] = 1000; //设置哨兵
R[n2] = 1000; //设置哨兵
//cout << "R[5] =" << R[4] << endl;
int k1 = 0;
int k2 = 0;
for (int k = first; k <= end; ++k) //把得到的两个数组进行排序合并
{
//cout << L[k1] <<endl;
//cout << R[k2] <<endl;
if(L[k1] <= R[k2])
{
//cout << L[k1] <<endl;
array[k] = L[k1];
//cout << array[k] << endl;
//cout << "k1 =" << k1 << endl;
k1 = k1 + 1;
}else{
//cout << R[k2] <<endl;
array[k] = R[k2];
//cout << array[k] << endl;
//cout << "k2 =" << k2 << endl;
k2 = k2 + 1;
}
//cout << array[k] <<endl;
}
//printArray(array,10);
}
/*
* 分治算法
* 把一个数组从中间分成分开
* 然后进行排序
*/
void merge_sort(int array[],int first,int end)
{
if(first < end){
int center = (first + end)/2; //得到中间数
merge_sort(array,first,center);
merge_sort(array,center+1,end);
merge(array,first,center,end);
}
}
int main(int argc, char const *argv[])
{
int array[10] = {0,6,1,2,3,7,8,9,4,5};
//merge(array,0,4,9);
merge_sort(array,0,9);
printArray(array,10);
//int center = (0 + 9)/2;
//cout << "center" << center << endl;
//cout << "hello";
return 0;
}
堆和堆排序
堆是一种特殊的完全二叉树
如果你是初学者,你的表情一定是这样的🤔
别想复杂
首先,你一定见过这种图
咱们暂时不管数字
这就是一个堆
堆又分为最大堆和最小堆
最大堆
看这张图
上面的节点的数都比下面的节点的数大,最上面的数是最大的,这就叫最大堆
最小堆
还是一样的数,看这张图
这是一个最小堆,同最大堆,最上面的节点的数最小,上面的节点的数比下面的节点的数大
怎么样,是不是很简单?
堆排序
堆排序的基本思想是利用堆,使在排序中比较的次数明显减少使速度更快
堆排序的时间复杂度为O(n*log(n)), 非稳定排序,原地排序(空间复杂度O(1))。
堆排序的关键在于建堆和调整堆,下面简单介绍一下建堆的过程:
可以用STL下的
make_heap()
具体步骤:
第1趟将索引0至n-1处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的最后一个节点交换,就使的这组数据中最大(最小)值排在了最后。
第2趟将索引0至n-2处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的倒数第二个节点交换,就使的这组数据中最大(最小)值排在了倒数第2位。
…
第k趟将索引0至n-k处的全部数据建最大(或最小)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的倒数第k个节点交换,就使的这组数据中最大(最小)值排在了倒数第k位。
其实整个堆排序过程中, 我们只需重复做两件事:
建堆(初始化+调整堆, 时间复杂度为O(n));
拿堆的根节点和最后一个节点交换(siftdown, 时间复杂度为O(n*log n) ).
因而堆排序整体的时间复杂度为O(n*log n)
没看懂可以看看这个图
代码
#include <iostream>
#include <stdlib.h>
using namespace std;
/*******************************************/
/* 堆排序
/******************************************/
void swap(int &a, int &b) //位置互换函数
{
int temp = a;
a = b;
b = temp;
}
void Heap(int array[], int length, int index) //堆排序算法(大顶堆)
{
int left = 2 * index + 1; //左节点数组下标
int right = 2 * index + 2; //右节点数组下标
int max = index; //index是父节点
if (left < length && array[left] > array[max]) //左节点与父节点比较
{
max = left;
}
if (right < length && array[right] > array[max]) //右节点与父节点比较
{
max = right;
}
if (array[index] != array[max])
{
swap(array[index], array[max]);
Heap(array, length, max); //递归调用
}
}
void HeapSort(int array[], int size) //堆排序函数
{
for (int i = size / 2 - 1; i >= 0; i--) // 创建一个堆
{
Heap(array, size, i);
}
for (int i = size - 1; i >= 1; i--)
{
swap(array[0], array[i]); //将array[0]的最大值放到array[i]的位置上,最大值往后靠
Heap(array, i, 0); //调用堆排序算法进行比较
}
}
int main(void) //主程序
{
const int n = 6; //数组元素的数量
int array[n];
cout << "请输入6个整数:" << endl;
for (int i = 0; i < n; i++)
{
cin >> array[i];
}
cout << endl; //换行
HeapSort(array, n); // 调用HeapSort函数 进行比较
cout << "由小到大的顺序排列后:" << endl;
for (int i = 0; i < n; i++)
{
cout << "Array" << "[" << i << "]" << " = " << array[i] << endl;
}
cout << endl << endl; //换行
system("pause"); //调试时,黑窗口不会闪退,一直保持
return 0;
}
最后
今天写了一万多字写出了这篇完整的总结,希望对大家有帮助。
如果有错误,请在评论区告诉我,我会改正的。
如果有问题可以私信我,我看到后会回答的。