数据结构中最全的8种排序算法总结
常见的排序算法有:
插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序,下面一一介绍:
1.插入排序
插入排序的基本思想是:每一步将一个待排序元素按其关键字值得大小插入到已排序序列得适当位置上,直到待排序元素插入完为止。如果要对具有n个元素得数组arr进行排序,初始状态时,可以认为已排序序列为arr[0],待排序序列为arr[1]~arr[n-1],从arr[i]开始向arr[0]方向扫描各元素,寻找适当位置插入arr[i],依次进行,即可完成排序。
代码如下:
void insertSort(int* arr,int n)
{
//假设第一个数据有序
//未插入的数据:(1,n)
for (int i = 1; i < n; ++i)
{
//从有序数据的最后一个位置向前遍历
int end = i - 1;
int data = arr[i];
while (end >= 0 && arr[end] >= data)
{
//大的数据向后移动
arr[end + 1] = arr[end];
--end;
}
arr[end + 1] = data;
}
}
插入排序总结:
1.元素集合越接近有序,直接插入排序算法的时间效率越高。
2.时间复杂度:O(N^2)
3.空间复杂度:O(1)
4.稳定性:稳定
2.希尔排序
希尔排序法的基本思想是:先选定一个整数gap值,把待排序文件中所有记录分成个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当gap=1时,所有记录在统一组内排好序。
代码如下:
void shellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)//当gap等于1的时候变为插入排序
{
gap = gap / 3 + 1;
//一趟希尔排序
for (int i = gap; i < n; ++i)
{
int end = i - gap;//同一组数据,最后一个有序数据的位置
int data = arr[i];//待插入的数据
while (end >= 0 && arr[end]>data)
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = data;
}
}
}
希尔排序总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
- 希尔排序的时间复杂度不好计算,需要进行推导,平均时间复杂度:O(N1.3—N2)
- 稳定性:不稳定
3.选择排序
选择排序思想是:每次从待排序序列中选择一个关键字最小的元素(当需要按关键字升序排列时),顺序排在已排序序列的最后,直至全部排完。而选择排序方法中最简的就是直接选择排序,就是通过顺序比较找出待排序序列中的最小元素。
代码如下:
void selectSort(int* arr, int n)
{
int start = 0;//从未排序的序列中找最值,存放到未排序的起始位置
int end = n - 1;//未排序的区间[start,end]
while (start < end)
{
int minIdx = start;//找到最小值得位置
for (int i = start + 1; i <= end; ++i)
{
if (arr[i] < arr[minIdx])
minIdx=i;
}
Swap(arr,start,minIdx);//把最小值存开始的位置
++start;//剩余的未排序的区间[start+1,end]
}
}
选择排序总结:
- 选择排序好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
4.堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
代码如下:
//向下调整
void shiftDown(int* arr, int n, int parent)
{
int child = 2 * parent + 1;
while (child < n)
{
if (child + 1 < n&&arr[child + 1] > arr[child])
++child;
if (arr[child]>arr[parent])
{
Swap(arr, child, parent);
parent = child;
child = 2 * parent + 1;
}
else
break;
}
}
//堆排序
void heapSort(int* arr, int n)
{
//建堆的过程(向下调整):建大堆
for (int i = (n - 2) / 2; i >= 0; --i)
{
shiftDown(arr,n,i);
}
int end = n - 1;
while (end > 0)
{
Swap(arr, end, 0);
shiftDown(arr,end,0);
--end;
}
}
堆排序总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
5.冒泡排序
基本思想是:(1)对具有n个元素的序列按升序进行冒泡排序,首先将第一个元素与第二个元素进行比较,若为逆序,则将两元素交换。然后比较第二、第三个元素,以此类推,直到第n-1和第n个元素进行比较和交换。此过程为第一趟起泡排序。经过第一趟最大的元素便被交换到了第n的位置。
(2)对前n-1个元素进行第二趟起泡排序,将其中最大元素交换到第n-1个位置。
(3)如此继续,直到某一趟排序未发生任何交换时,排序完毕。对n个元素的序列,起泡排序最多需要n-1趟。在这个排序过程中小的气泡往上浮,大的气泡往下沉,就像冒泡一样,因此称为冒泡排序。
代码如下:
void bubbleSort(int* arr, int n)
{
int end = n;//每一次遍历范围:[0,未排序数据的最后一个位置]
while (end>1)
{
int flag = 0;//标记一轮冒泡排序中是否发生了交换操作
//一轮冒泡排序
for (int i = 1; i < end; ++i)
{
if (arr[i - 1]>arr[i])
{
Swap(arr,i-1,i);//大的向后移动
flag = 1;
}
}
if (!flag)//说明剩余元素全部有序
break;
--end;
}
}
冒泡排序总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
6.快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare版本
2. 挖坑法
3. 前后指针版本
基准值获取方法:三数去中法 :即起始,中间,结束。
代码如下:
int getMid(int* arr, int begin, int end)
{
int mid = begin + (end - begin) / 2;
if (arr[begin] > arr[end])
{
if (arr[mid] > arr[end])
return mid;
else if (arr[begin] > arr[end])
return end;
else
return begin;
}
else
{
if (arr[mid] < arr[end])
return mid;
else if (arr[begin] < arr[end])
return end;
else
return begin;
}
}
第一种基准值划分的方式:hoare版本,代码如下:
int partion1(int* arr, int begin, int end)
{
int mid = getMid(arr,begin,end);//获取基准值的位置
Swap(arr,begin,mid);
int key = arr[begin];//基准值的数据
int start = begin;
while (begin < end)
{
//从后向前找小于基准值的位置
while (begin<end && arr[end] >= key)
--end;
//从前向后找大于基准值的位置
while (begin<end && arr[begin] <= key)
++begin;
Swap(arr, begin, end);
}
//相遇位置的数据和基准值数据交换
Swap(arr,start,begin);
return begin;
}
第二种基准值划分的方式:挖坑法,代码如下:
int partion2(int* arr, int begin, int end)
{
//第一个值作为基准值,第一个位置为初始的坑的位置
int mid = getMid(arr,begin,end);
Swap(arr,begin,end);
int key = arr[begin];
while (begin < end)
{
//从后向前找小的
while (begin < end&&arr[end] >= key)
--end;
//填坑
arr[begin] = arr[end];
//从前向后找大的
while (begin < end&&arr[begin] <= key)
++begin;
//填坑
arr[end] = arr[begin];
}
arr[begin] = key;
return begin;
}
第三种基准值划分的方式:前后指针版本,代码如下:
int partion3(int* arr, int begin, int end)
{
int mid = getMid(arr, begin, end);
Swap(arr, begin, end);
//上一个小于基准值的位置
int prev = begin;
//下一个小于基准值的位置
int cur = begin + 1;
int key = arr[begin];
while (cur <= end)
{
//当cur走到下一个小于基准值的位置,判断prev和cur是否连续
if (arr[cur] < key&& ++prev != cur)
{
//不连续则交换数据:prev和cur数据交换
Swap(arr,prev,cur);
}
++cur;
}
//cur走到结尾,则交换pre和key的数据,最后将数据划分为两部分
Swap(arr,begin,prev);
return prev;
}
例如:以递归法用hoare法划分基准值的方式的快速排序
void quickSort(int* arr, int begin, int end)
{
if (begin >= end)
return;
//div是一次划分之后,基准值位置
//partion1函数是hoare法,可改为partion2挖坑法,partion3前后指针法
int div = partion1(arr,begin,end);
//基准值左右两部分进行快速排序
//[begin,div-1]
quickSort(arr,begin,div-1);
//[div+1,end]
quickSort(arr, div + 1, end);
}
快速排序总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
7.归并排序
归并排序(merge-sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
代码如下:
//归并排序递归方式
void merge(int* arr, int begin, int mid, int end, int* tmp)
{
//子区间:[begin,mid] [mid+1,end]
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
//辅助空间的起始位置
int idx = begin;
//合并有序序列
while (begin1 <= end1&&begin2 <= end2)
{
if (arr[begin1] <= arr[begin2])
tmp[idx++] = arr[begin1++];
else
tmp[idx++] = arr[begin2++];
}
//判断是否有未合并的元素
if (begin1 <= end1)
memcpy(tmp+idx,arr+begin1,sizeof(int)*(end1-begin1+1));
if (begin2 <= end2)
memcpy(tmp+idx,arr+begin2,sizeof(int)*(end2-begin2+1));
//合并之后的序列考到原始数组的对应区间
memcpy(arr+begin,tmp+begin,sizeof(int)*(end-begin+1));
}
void _mergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = begin + (end-begin) / 2;
//首先分解成子序列
_mergeSort(arr,begin,mid,tmp);
_mergeSort(arr,mid+1,end,tmp);
//合并两个有序的子序列[begin,mid] [mid+1,end]
merge(arr,begin,mid,end,tmp);
}
void mergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
_mergeSort(arr,0,n-1,tmp);
free(tmp);
}
归并排序非递归法:
void mergeSortNoR(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
//子序列的步长
int step = 1;
while (step < n)
{
for (int idx = 0; idx < n; idx += 2 * step)
{
//找到两个待合并的子序列区间
int begin = idx;
int mid = idx + step - 1;
//判断是否存在第二个子序列
if (mid >= n - 1)
continue;
int end = idx + 2 * step - 1;
//判断第二个子序列是否越界
if (end >= n)
end = n - 1;
merge(arr, begin, mid, end, tmp);
}
//更新步长
step *= 2;
}
}
归并排序总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
8.计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:1. 统计相同元素出现次数。2. 根据统计的结果将序列回收到原来的序列中。
void countSort(int* arr, int n)
{
//找到最大和最小值
int max, min;
min = max = arr[0];
for (int i = 1; i < n; ++i)
{
if (arr[i]>max)
max = arr[i];
if (arr[i] < min)
min = arr[i];
}
//计算范围
int range = max - min + 1;
//创建一个计数数组,初始化为0
int* countArr = (int*)calloc(range,sizeof(int));
//计数
for (int i = 0; i < n; ++i)
{
countArr[arr[i] - min]++;
}
//遍历计数数组,
int idx = 0;
for (int i = 0; i < range; ++i)
{
while (countArr[i]--)
{
arr[idx++] = i + min;
}
}
}
计数排序总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定