常见排序算法
一、直接插入排序:
插入排序是从数组的首元素开始,遍历整个数组,遍历到每个元素的时候,将这个元素依次和它前面的元素进行比较,如果按照从小到大的方式进行排序的话,要求前面的所有元素的值小于它本身,使用一个中间变量,将这个元素保存起来,将大于它的元素依次往后移,最后遇到和它一样大或者小于它的元素,将它放到这个元素的后面。所以排序过程中,需要一个临时的中间变量。
代码:
void sort(int* src, int n)
{
int i, j;
int tmp;
for (i = 0; i < n; ++i)
{
tmp = src[i];
for (j = i; j > 0 && src[j - 1] > tmp; --j)
{
/*插入排序的核心思想是这个部分,含义是要排序的第i位也就是tmp值
如果前面有比他还大的数那就将这些数后移一个,给tmp应该在的地方腾出来
*/
src[j] = src[j - 1];
}
src[j] = tmp;
/* 最后再将tmp插进去*/
}
}
直接插入排序特性总结:
1、元素集合越接近有序,直接插入排序的时间复杂度越低。
2、时间复杂度:O(N^2)。
3、空间复杂度:O(1),它是一种稳定的排序算法。
4、稳定性:稳定
2、希尔排序(缩小增量排序)
希尔排序也是一种插入排序,是插入排序后的种更加高效的排序方式,由于插入排序,被排序序列越有序插入排序越快。
图片来自 https://blog.csdn.net/dujiafengv/article/details/102599103
代码:
#include<stdio.h>
#include<stdlib.h>
void shellSort(int arr[], int length)
{
int temp = 0;
int count = 0;
// 根据前面的逐步分析,使用循环处理
for (int gap = length / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < length; i++)//一对一对往后走
{
// 遍历各组中所有的元素(共gap组,每组有个元素), 步长gap
for (int j = i - gap; j >= 0; j -= gap)//一组的前面一个,
{
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap])
{
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
int main()
{
int arr[6] = { 5,0,1,6,10,2 };
shellSort(arr, 6);
for (int i=0;i<6;++i)
{
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
希尔排序特性总结:
1、希尔排序是对直接插入排序的优化
2、当排序步长都>1的排序都是预排序,目的是让数组更加接近有序,当步长==1的时候,数组已经很接近有序了,这样排序的速度就很快了。
3、希尔排序的时间复杂度不确定,经过推导出来的时间复杂度为O(N^1.3-N^2)。
4、稳定性:不稳定。
3、选择排序:
代码:
#include<stdio.h>
#include<stdlib.h>
void SelectionSort(int arr[], int length)
{
int _min = 0;
int tmp;
int index;
for (int i = 0; i < length; ++i)
{
_min = arr[i];
for (int j = i + 1; j < length; ++j)
{
if (arr[j] < _min)
{
_min = arr[j];
index = j;
}
}
tmp = arr[i];
arr[i] = arr[index];
arr[index] = tmp;
}
}
int main()
{
int arr[6] = { 5,0,1,6,10,2 };
SelectionSort(arr, 6);
for (int i = 0; i < 6; ++i)
{
printf("%d ", arr[i]);
}
system("pause");
return 0;
}
直接选择排序的特性总结:
1、直接选择排序的算法效率不高,实际很少使用。
2、时间复杂度O(N^2)。
3、空间复杂度:O(1)。
4、稳定性:不稳定。
4、堆排序。
堆排序是借助一种特殊的数据结构来排序的,要使用堆,首先需要建堆,堆在逻辑上是一颗完全二叉树,在内存中是一个数组(数组是按照二叉树的层序遍历存储的)。堆只分为大顶堆或者小顶堆,建堆的过程需要用到自顶向下调整算法。
自顶向下调整算法:
/*返回以index为根的完全二叉树的左子树的索引,整个二叉树索引以0为开始*/
int left(int index)
{
return ((index << 1) + 1);
}
/*返回以index为根的完全二叉树的右子树的索引,整个二叉树索引以0为开始*/
int right(int index)
{
return ((index << 1) + 2);
}
/*两个数的交换*/
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
return;
}
void max_heap_adjust(int array[], int index)
{ //index是位置
int left_index = left(index);
int right_index = right(index);
int largest = index;
//左子树和父节点进行对比
if (left_index <= (heap_size - 1) && array[left_index] > array[largest]) {
largest = left_index;
}
//右子树和父节点进行对比
if (right_index <= (heap_size - 1) && array[right_index] > array[largest]) {
largest = right_index;
}
if (largest == index) {
/*判断是否要进行递归调用,每交换一次最小二叉树的时候,可能会破坏前面已经调整好的堆
的结构,所以交换一次需要从当前父亲节点开始重新进行自顶向下算法,重新调整堆*/
/*这里的递归退出条件是 经过上面的计算最大值仍然是堆顶,即调整不能调整为止
*/
return;
}
else {
//需要交换
swap(&array[index], &array[largest]);
//递归调用
max_heap_adjust(array, largest);
}
}
更加详细的建堆过程 https://blog.csdn.net/weixin_43447989/article/details/99695304
建立好大顶堆或者小顶堆以后,就可以进行堆排序了。
方式是,依次将堆顶的元素与堆尾的元素交换,交换后的堆尾作为有序区不再参与堆的自顶向下调整算法。可以发现,要得到递增序列初始的堆必须为大顶堆,递减序列,初始的堆为小顶堆。
代码:
/*返回以index为根的完全二叉树的左子树的索引,整个二叉树索引以0为开始*/
int left(int index)
{
return ((index << 1) + 1);
}
/*返回以index为根的完全二叉树的右子树的索引,整个二叉树索引以0为开始*/
int right(int index)
{
return ((index << 1) + 2);
}
/*两个数的交换*/
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
return;
}
void max_heap_adjust(int array[], int index)
{ //index是位置
int left_index = left(index);
int right_index = right(index);
int largest = index;
//左子树和父节点进行对比
if (left_index <= (heap_size - 1) && array[left_index] > array[largest]) {
largest = left_index;
}
//右子树和父节点进行对比
if (right_index <= (heap_size - 1) && array[right_index] > array[largest]) {
largest = right_index;
}
if (largest == index) {
/*判断是否要进行递归调用,每交换一次最小二叉树的时候,可能会破坏前面已经调整好的堆
的结构,所以交换一次需要从当前父亲节点开始重新进行自顶向下算法,重新调整堆*/
/*这里的递归退出条件是 经过上面的计算最大值仍然是堆顶,即调整不能调整为止
*/
return;
}
else {
//需要交换
swap(&array[index], &array[largest]);
//递归调用
max_heap_adjust(array, largest);
}
}
void heap_sort(int array[], int length) {
int old_heap_size = heap_size;
int i;
for (i = length - 1; i >= 1; --i) {
swap(&array[i], &array[0]);
--heap_size;
max_heap_adjust(array, 0);
}
//恢复堆的大小
heap_size = old_heap_size;
}
建堆的过程需要的时间复杂度是O(logn),堆排序最坏的情况是排N次,所以堆排序的时间复杂度是O(Nlogn),堆排序仅仅需要一个记录原始堆大小的辅助变量。
堆排序的特性分析:
1、使用堆排序来选择第几大的数很好用。
2、时间复杂度:O(logn)。
3、空间复杂度:O(1)。
4、稳定度:不稳定。
5、交换排序
基本思想: 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
5.1 冒泡排序
代码:
#include<stdio.h>
#include<stdlib.h>
#define MAX 3
//冒泡排序
int main()
{
int a[MAX] = { 9,6,14 };
int i = 0;
int t = 0;
int j = 0;
for (i = 0; i < MAX; i++)
{
for (j = 0; j < (MAX-1) - i; j++)//此处的MAX-1-i不是MAX-i的原因是j+1不会越界;
{
if (a[j + 1] > a[j])//外层循环循环一次就会把较大数往前移一位
{
t = a[j + 1];
a[j + 1] = a[j];
a[j] = t;
}
}
}
for (i = 0; i < MAX; i++)
{
printf("%d ", a[i]);
}
system("pause");
return 0;
}
对于冒泡排序相信大家都比较熟悉,它的核心思想是,假如是将数组从大到小排序,外层for循环执行一次,数组中最大的那个数将向前移一位。所以外层for循环的循环次数最多是数组的的元素个数次。而内层for循环的循环次数如for (j = 0; j < (MAX-1) - i; j++)//此处的MAX-1-i不是MAX-i的原因是j+1不会越界,因为后面会有a[j+1]和a[j]的判断。
冒泡排序的特性分析:
1、时间复杂度:O(N^2)。
2、空间复杂度:O(1)。
3、稳定度:稳定。
6、快速排序:
6.1、 hoare法
实现步骤:
第一步:选择待排序数组中的三个值,分别是首尾还有中值,start,end 和mid
第二步:对三个数进行排序,从小到大,
第三步:保护基准值,swap(src + mid, src + end - 1);也就是基准值和倒数第二个元素交换
第四步:a = start + 1, b = end - 2,b向前找比基准值小的,a向后找比基准值大的,找到之后 交换所指向的值,然后将基准值和a指向的值交换 (因为是从小到大排序a找的大的,应该在数组后面所以基准值和它换),产生左边的都比基准值小,右边的都比基准值大,然后a作为二叉树的根返回,重复上面四步操作,直到类似形状的二叉树被遍历完为止。
int hoareway(int* src, int start, int end)
{
int a = start + 1, b = end - 2;
int mid = (start + end) / 2;
if (src[start] > src[mid])
{
swap(src + start, src + mid);
}
if (src[mid] > src[end])
{
swap(src + mid, src + end);
}
if (src[start] > src[mid])
{
swap(src + start, src + mid);
/*上面是三数排序部分*/
}
if (end - mid <= 2)
{
return mid;
/*如果小于4个数直接返回输出*/
}
/*保护基准值*/
swap(src + mid, src + end - 1);
while (a <= b)
{
while (a < end - 1 && src[a] <= src[end - 1])
{
a++;
}
while (b > 0 && src[b] >= src[end - 1])
{
b--;
}
if (a == b && (a == 1 || a == end - 1))
{
break;
/*一种是找到同一个值,一种是找到了已经三数排好的地方,此时就不用排*/
}
if (a < b)
{
swap(src + a, src + b);
/* 交换*/
}
}
swap(src + a, src + end - 1);//将基准值交换回来
return a;
}
void dealquicksort(int *src, int start, int end)
{
int mid;
if (start < end)
{
mid = hoareway(src, start, end);
dealquicksort(src, start, mid - 1);
dealquicksort(src, mid + 1, end);
}
}
void quicksort(int *src, int n)
{
//快速排序
dealquicksort(src, 0, n - 1);
}
6.2 双指针法
双指针法:
从小到大
第一步:定义两个指针一个指向数组头a一个指向数组尾b;
第二步:以头指针为基准值先从尾指针开始向前找比基准值小的元素,找的之后交换a,b所指向的值
第三步:此时b所指向的值就是基准值,a开始向后找,找到比基准值大的交换。
第四步:完成一次交换之后,以基准值下标为返回值返回递归调用点
void swap(int* a, int*b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int doublepointway1(int *src, int start, int end)
{
int a = start;
int b = end;
int flag = 0;
while ( src[b] > src[a])
{
b--;
}
while (a < b)
{
swap(src + b, src + a);
flag = !flag;
while (src[b] >= src[a])
{
flag ? a++ : b--;
/* tlag作为标记位,用来判断是让a指针寻找还是让b指针寻找*/
}
}
return flag ? b : a;
}
void dealquicksort(int *src, int start, int end)
{
int mid;
if (start<end)
{
mid = doublepointway2( src, start, end);
dealquicksort( src, start, mid-1);
dealquicksort(src, mid + 1, end);
}
}
void quicksort(int *src, int n)
{
//快速排序
dealquicksort(src, 0, n-1);
}
挖坑法:https://blog.csdn.net/weixin_43447989/article/details/100054493
快速排序的特性总结:
1、快速排序的综合性能和使用场景都是很好的,所以叫快速排序。
2、时间复杂度,排一次的时间复杂度是O(logn),最坏的情况下排n次,所以整体时间复杂度是O(n*logn)。
3、空间复杂度:O(logn)。
4、稳定性:不稳定。
7、归并排序
图片来自https://blog.csdn.net/doubleguy/article/details/81390951
可以看到归并排序的基本方法是先对数组执行分的操作,将数组分割成二叉树的样子,再对其进行排序。
图片来自https://blog.csdn.net/weixin_44465743/article/details/88902052
从上面的分治图中可以总结出规律:将数组细分为最小单元时,形状像一颗二叉树,并且它的排序方式和二叉树的后序递归遍历很像,我们尝试着使用二叉树的后序递归遍历解决基本的代码框架。
二叉树的后序遍历是:
void BinaryTreePostOrder(BTNode* root)
{
if (root)
{
BinaryTreePostOrder(root->lchild);
BinaryTreePostOrder(root->rchild);
putchar(root->data);
}
}
代码实现:
void dealMergeSort(int *src, int* tmp, int str, int end)
{
if (str >= end)
{
return;
}
int mid = (str + end) / 2;
dealMergeSort(src, tmp, str, mid);
dealMergeSort(src, tmp, mid + 1, end);
int a = str;
int b = mid + 1;
int c = str;//1,5,6,15,8,26,5,55,9,0
while (a <= mid && b <= end)
{
if (src[a] > src[b])
{
tmp[c] = src[a];
a++;
}
else
{
tmp[c] = src[b];
b++;
}
c++;
//将两个已经比较好的数据存入临时的空间里面
}
for (; a <= mid; a++, c++)
{
tmp[c] = src[a];
}
for (; b <= end; b++, c++)
{
tmp[c] = src[b];
}
//将剩余的数据存到临时数组里面
int i = 0;
for (i = 0; i <= end; ++i)
{
src[i] = tmp[i];
}
//将临时数组元素存入到原数组当中
}
void MergeSort(int *src, int n)
{
int *tmp = (int*)malloc(n * sizeof(int));
dealMergeSort(src, tmp, 0, n - 1);//传入下标
free(tmp);
}
归并排序特性分析:
1、归并排序需要额外的空间开销。
2、时间复杂度:O(nlogn)。
3、空间复杂度O(N)
4、稳定性:稳定。
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 稳定 |
插入排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N*logN)~O(N^2) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
快速排序 | O(N*logN) | O(N*logN) | O(N^2) | O(logN)~O(N) | 不稳定 |