算法名称 | 最好时间复杂度 | 平均时间复杂度(一般不好算) | 最坏时间复杂度 | 稳定性 | 空间复杂度 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | 稳定 | |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | 不稳定 | |
直接插入排序 | O(n) | O(n^2) | O(n^2) | 稳定 | |
希尔排序 | O(n) | O(n^2) | 不稳定 | ||
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | 不稳定 | O(1) |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | 不稳定 | 最好O(logn) 最坏O(N) 平均O(logn) |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | 稳定 | O(N) |
一、插入排序
插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌
对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
void insert_sort(int a[], int N)
{
int i, j, key;
for (i = 1; i < N; i++)//将要插入的数从下标1开始,0位置元素默认是拍好的
{
key = a[i];
for (j = i - 1; j >= 0 &&a[j]>key;j-- )
{
a[j + 1] = a[j];
}
a[j + 1] = key;
}
}
注意:上面的简单选择排序不要写成了
void insert_sort(int a[], int N)
{
int i, j, key;
for (i = 1; i < N; i++)//将要插入的数从下标1开始,0位置元素默认是拍好的
{
key = a[i];
for (j = i - 1; j >= 0; j--)
{
if (a[j] > key)
{
a[j + 1] = a[j];
}
}
a[j + 1] = key;
}
}
这种写法不好,因为当碰到第一个a[j]<=key时,前面的就不用比较了。省时间
时间复杂度:
最好:序列是有序的时候,第二个for循环里面内容不执行,复杂度O(n)
最坏:O(n^2)
平均:O(n^2)
稳定性:稳定
二分插入排序,二分(折半)插入(Binary insert sort)排序基本思想:设在数据表中有一个元素序列v[0],v[1],v[2]......v[n].其中v[0],v[1],v[2]......v[i-1]是已经排好序的元素。在插入v[i]。利用折半搜索寻找v[i]的插入位置。
void BinaryInsert_sort(int a[], int N)
{
for (int i = 1; i < n; i++)
{
int get = A[i]; // 右手抓到一张扑克牌
int left = 0; // 拿在左手上的牌总是排序好的,所以可以用二分法
int right = i - 1; // 手牌左右边界进行初始化
while (left <= right) // 采用二分法定位新牌的位置,left就是新牌应该插入的位置
{
int mid = (left + right) / 2;
if (A[mid] > get)
right = mid - 1;
else
left = mid + 1;
}
for (int j = i - 1; j >= left; j--) // 将欲插入新牌位置右边的牌整体向右移动一个单位
{
A[j + 1] = A[j];
}
A[left] = get; // 将抓到的牌插入手牌
}
}
#插入排序,自己写的怎么就这么差了
#include<stdio.h>
void insert_sort(int a[],int length)
{
int key;
for (int i = 1; i < length; i++)//将要插入的数从下标1开始,0位置元素默认是拍好的
{
key = a[i];
for (int j = i-1; j >= 0; j--)//已排序元素从后向前遍历,依次和要插入的数比较大小
{
if (a[j] > key)//当前元素大于要插入的元素时,交换两者的值
{
a[j+1] = a[j];
if (j == 0)//注意第一个元素别遗漏
{
a[j] = key;
}
}
else
{
a[j + 1] = key;//当前元素小于要插入的元素时,当前元素的后一位赋值为要插入的值
break;
}
}
}
}
int main()
{
int a[10];
int N = 10;
for (int i = 0; i < N; i++)
{
scanf("%d", &a[i]);
}
insert_sort(a, 10);
for (int i = 0; i < N; i++)
{
printf("%d ", a[i]);
}
return 0;
}
二、冒泡排序
//冒泡排序
void bubble(int a[], int N)
{
for (int i = 0; i < N - 1; i++)//
{
for (int j = 0; j < N - 1-i; j++)
{
if (a[j] > a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
时间复杂度:
看网上说冒泡排序最好的情况下时间复杂度为O(n),当时就比较疑惑,就算序列原本是有序的,也要进行O(n^2)的a[j]和a[j+1]的大小比较呀,原来他们说的O(n)针对的是冒泡排序的优化版本。
最差时间复杂度:O(n^2)
平均时间复杂度:O(n^2)。说实话冒泡排序优化版本平均时间复杂度我不会算,百度了也没有发现推导的,都是直接给结论的。
冒泡排序的优化版本
//冒泡排序优化,序列有序时,时间复杂度为O(n)
void Bubble(int a[], int N)
{
int flag;
for (int i = 0; i < N - 1; i++)
{
flag = 0;//这里加一条这个比较好
for (int j = 0; j < N - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = 1;
}
}
if (flag == 0)
{
return;
}
}
}
三、选择排序
//选择排序
void select_sort(int a[], int N)
{
for (int i = 0; i < N - 1; i++)
{
for (int j = i+1; j < N; j++)
{
if (a[i] > a[j])
{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
当初稀里糊涂学历几个月C#,这里吧C#版本的冒泡和选择一并放在这里
C#版//冒泡排序
public static void Bubble(int []arr)
{
for (int i = 0; i < arr.Length-1; i++)
{
for (int j = 0; j < arr.Length-1-i; j++)
{
if(arr[j]>arr[j+1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
//选择排序
public static void Select_Sort(int[] arr)
{
for (int i = 0; i < arr.Length-1; i++)
{
for (int j = i+1; j < arr.Length; j++)
{
if(arr[i]>arr[j])
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
哈哈哈哈,原来我以前写的选择排序一直是盗版的选择排序,正确的现则排序应该是这样的。
思想:从要排序的序列中选择最小的数,把它放在第一个位置;在从剩下的序列中选择最小的数,把它放在第二个位置。。。
我以前写的时候,每次找到后面的数比前面的数大,就进行了交换,其实不用,一次循环只用找到最小的数之后,交换一次就好。
//正确的选择排序
void SelectSort(int a[], int N)
{
int min;
for (int i = 0; i < N - 1; i++)
{
min = i;
for (int j = i+1; j < N; j++)
{
if (a[j]<a[min])
{
min = j;
}
}
if (min !=i)
{
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
时间复杂度:
不管怎么样,都要进行两个for循环,时间复杂度都一样,都是O(n^2)。
稳定性:
不稳定,比如序列{5,8,5,2,9},第一个排序的时候,将第一个5和2交换,改变了两个5的相对次序,导致不稳定。
四、快速排序
快速排序稍微复杂一点,先选一个标尺,用它把整个队列过一遍,以保证:其左边的元素都不大于它,其右边的元素都不小于它。这样,排序问题就被分割为两个字区间。再分别对子区间排序就可以了。
//快速排序
#include<stdio.h>
void swap(int *a, int x, int y) {
int temp = a[x];
a[x] = a[y];
a[y] = temp;
}
void quicksort(int *a, int left, int right)
{
if (left >= right)
{
return;
}
int key = a[left];
int i = left;
int j = right;
while (i <j)
{
while (i<j && (a[--j] >= key))
{
}
while (i<j && a[++i] <= key)//这里要加i<j
{
}
swap(a,i,j);
}
swap(a,left,j);
quicksort(a, left, j);
quicksort(a, j + 1, right);
}
int main()
{
int i;
//int a[] = { 5,4,1,2,3,10,8,9,7,6 };
int N=10;
int a[10];
for (i = 0; i < N; i++)
{
scanf("%d", &a[i]);
}
quicksort(a, 0, N);
for (i = 0; i < N; i++)
{
printf("%d ", a[i]);
}
}
鸡尾酒排序
鸡尾酒排序即定向冒泡排序,是冒泡排序的轻微变形。
它的主要思路是对于一组数字,先找到最大的数字放到最后一位,再反向找到最小的数字放到第一位。然后再找到第二大数数字放到倒数第二位,再找到第二小的数字放到第二位,依次类推。
图片摘自https://www.cnblogs.com/eniac12/p/5329396.html
//鸡尾酒排序
void cocktail_sort(int a[], int N)
{
int left = 0;
int right = N-1;
while (left < right)
{
for (int i = left; i < right; i++)
{
if (a[i] > a[i+1])
{
int temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
}
}
right--;
for (int j = right; j > left; j--)
{
if (a[j - 1] > a[j])
{
int temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
}
}
left++;
}
}
呜呜呜,我自己下的鸡尾酒排序一开始是这样的,差别怎么这么大了。
//自己的鸡尾酒
void jiweijiu(int a[], int N)
{
for (int i = 0; i < N-1; i++)
{
if ((i % 2 )== 0)
{
for (int j = (i/2); j < N - 1 - (i/2); j++)
{
if (a[j] > a[j + 1])
{
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
else
{
for (int j = N - 1 - (i/2+1); j > i/2; j--)
{
if (a[j] < a[j - 1])
{
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
}
}
}
五、 归并排序
归并:把两个已经排好序的数组合并成一个有序数组
步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
//当有两个数组需要归并时,
#include<stdio.h>
#define N 5
#define M 4
void merge(int *p,int *a, int *b,int n,int m)//归并操作。尽量不要改变p所指向的值
{
int i = 0;
int j = 0;
int index=0;
while (i < n && j < m)
{
if (*(a+i) <= *(b+j))
{
p[index++]= a[i++]
}
else
{
p[index++] = b[j++];
}
}
while (i < n) //1式.....b先排完就访问,1,2式两者只有一个执行
{
p[index++]= a[i++]
}
while (j < m)//2式.....a先排完就访问,1,2式两者只有一个执行
{
p[index++] = b[j++];
}
}
int main(void)
{
int a[N] = { 1,2,3,4,5 };//a,b为已排序数组
int b[M] = { 4,5,6,7 };
int result[N + M];
merge(result,a, b,N,M);
for (int i = 0; i < N + M; i++)
{
printf("%d ", result[i]);
}
return 0;
}
归并排序中,最开始只有一个数组,所以需要将一个数组从中间分成两部分,假设左边和右边都是排好序的,对齐进行归并。
void merge(int a[], int left, int mid, int right)
{
int len = right - left + 1;
int i = left;
int j = mid + 1;
int *temp = (int *)malloc(sizeof(int)*(right - left + 1));
int index = 0;
while (i <= mid && j <= right)
{
temp[index++] = a[i] <= a[j] ? a[i++] : a[j++]; // 带等号保证归并排序的稳定性
}
while (i <= mid)
{
temp[index++] = a[i++];
}
while (j <= right)
{
temp[index++] = a[j++];
}
for (int k = 0; k < len; k++)
{
a[left++] = temp[k];//注意这里是left++,left在变,len不能换成right-left+1.
}
}
归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。
#include<stdio.h>
#include<math.h>
void merge(int a[], int left, int mid, int right)
{
int len = right - left + 1;
int i = left;
int j = mid + 1;
int *temp = (int *)malloc(sizeof(int)*(right - left + 1));
int index = 0;
while (i <= mid && j <= right)
{
temp[index++] = a[i] <= a[j] ? a[i++] : a[j++]; // 带等号保证归并排序的稳定性
}
while (i <= mid)
{
temp[index++] = a[i++];
}
while (j <= right)
{
temp[index++] = a[j++];
}
len = right - left + 1;
for (int k = 0; k < len; k++)
{
a[left++] = temp[k];
}
}
void MergesortRecursion(int a[], int left, int right)//递归实现
{
if (left == right)
{
return;
}
int mid = (left + right) / 2;
MergesortRecursion(a, left, mid);
MergesortRecursion(a, mid + 1, right);
merge(a, left, mid, right);
}
void MergesortIteration(int a[],int len)//非递归实现,自底向上。
{
for (int i = 1; i < len; i*=2)
{
for (int left = 0; left< len; left += 2 * i)
{
int right = (left + 2 * i - 1) < (len - 1) ? (left + 2 * i - 1) : (len - 1);
merge(a, left, (left + right) / 2, right);
}
}
}
int main(void)
{
int a[] = { 2 ,4,6,8,9,1,3,5,7,10 };
int b[] = { 2 ,4,6,8,9,1,3,5,7,10 };
MergesortRecursion(a, 0, 9);
MergesortIteration(b, 10);
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
printf("\n");
for (int i = 0; i < 10; i++)
{
printf("%d ", b[i]);
}
}
上面非递归方法,自己写的不好。把别人的粘贴到这里,看看吧。
void MergesortIteration(int a[],int len)//非递归实现,自底向上。
{
for (int i = 1; i < len; i*=2)
{
for (int left = 0; left+i< len; left += 2 * i)..............疑问
{
int right = (left + 2 * i - 1) < (len - 1) ? (left + 2 * i - 1) : (len - 1);
merge(a, left, (left + right) / 2, right);
}
}
}
感到疑问的是,迭代实现时,第二个for里面的判断条件为什么是left+i<len,不太懂。
六、希尔排序
希尔排序是基于插入排序的
参考https://www.cnblogs.com/chengxiao/p/6104371.html
https://www.cnblogs.com/eniac12/p/5329396.html#s4
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
#include<stdio.h>
void sheelsort(int a[], int len)//希尔排序
{
int h = len / 2;//这里,设置增量为len/2,len/2/2...
while (h >= 1)
{
for (int i = h; i < len; i++)//前h个数默认已经排好
{
int get = a[i];//把当前要插入的数拿出来,因为在下面a[j+h]=a[j]中会改变a[i]的值
int j;
for (j = i - h; j >= 0 && a[j]>get; j -= h)//注意这里判断条件
{
a[j + h] = a[j];
}
a[j + h] = get;
}
h /= 2;
}
}
int main(void)
{
int a[] = { 5, 2, 9, 4, 7, 6, 1, 3,8 };
int len = sizeof(a) / sizeof(int);
sheelsort(a, len);
for (int i = 0; i < len; i++)
{
printf("%d ", a[i]);
}
}
//希尔排序错误写法
void sheelsort(int a[], int len)//希尔排序
{
int h = len / 2;//这里,设置增量为len/2,len/2/2...
while (h >= 1)
{
for (int i = h; i < len; i++)//前h个数默认已经排好
{
int get = a[i];//把当前要插入的数拿出来,因为在下面a[j+h]=a[j]中会改变a[i]的值
int j;
for (j = i - h; j >= 0 ; j -= h)//这里判断条件错误。如果一开始i=1,j=0,且a[1]>a[0],会导致a[0]=a[1],
{
if(a[j]>key)
{
a[j + h] = a[j];
}
}
a[j + h] = get;
}
h /= 2;
}
}
另一种sheelsort写法,其实也差不多。参考:
https://www.cnblogs.com/yonghao/p/5151641.html
又看了下,不要这样写。
void ShellSort(int array[],int length) {
int index = sizeof(array) / 2;
int temp = 0;
while (index >= 1) {
for (int i = index; i<length; i++) {
for (int j = i - index; j >= 0; j -= index) {
if (array[j]>array[j + index]) {//后一个比前一个大,就两两交换。
temp = array[j];
array[j] = array[j + index];
array[j + index] = temp;
}
}
}
index = index / 2;
}
}
时间复杂度:
当增量为1时,希尔排序退化成了直接插入排序,时间复杂度为O(n^2),
当然最好还是O(n)
一般情况下,复杂度与增量的选择有关,这涉及到数学上尚未解决的难题。网友都说的乱七八糟的。大概是O(n^1.几)的样子。
七、堆排序
堆也是一种数据结构,堆是具有下列性质的完全二叉树:
最大堆(大顶堆):每个结点的值都大于或者等于其左右孩子结点的值。用于升序排序
最小堆(小顶堆):每个结点的值都小于或者等于其左右孩子结点的值。用于降序排序
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
完全二叉树
设二叉树的深度为h,除第h层外,其它各层(0-h-1)层的结点数都达到最大个数,第h层的所有结点都连续集中在最左边。
堆常见操作:
- 上浮 shift_up;
- 下沉 shift_down
- 插入 push
- 弹出 pop
- 取顶 top
- 堆排序 heap_sort
下沉:当父节点的元素值小于左子节点或者小于右子节点时,将父节点的元素值与左右子结点较大的元素值进行交换,针对交换后的“父节点”,循环执行元素下沉操作。
上浮:将当前节点(初始节点)和它的父节点进行比较,若元素值比父节点小,就交换,针对交换后的初始节点,循环执行元素上浮操作。
懒得写了,常见操作见https://www.cnblogs.com/JVxie/p/4859889.html
堆排序:
思想:
- 将长度为n的序列构造成一个大顶堆,此时整个序列的最大值就是堆顶的根节点
- 将它和数组的末尾元素进行交换,此时末尾元素就是最大值
- 将剩余的n-1个元素重新构造成一个堆,这样就得到次大值
- 反复执行,就得到了一个有序序列。
图片示意过程见:
https://www.cnblogs.com/MOBIN/p/5374217.html
https://www.cnblogs.com/chengxiao/p/6129630.html
#include<stdio.h>
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void HeapAdjust(int a[], int start, int end)
{
int temp = a[start];
for (int s = 2 * start + 1; s <= end; s = 2 * start + 1) /* 沿关键字较大的孩子结点向下筛选 */
{
if (s+1<=end && a[s] < a[s + 1])
s++;/* j为关键字中较大的记录的下标 */
if (temp >= a[s])//注意这里为temp,不是a[start]
break;
a[start] = a[s];
start = s;
}
a[start] = temp;
}
//堆排序
void HeapSort(int a[], int len)
{
for (int i = len / 2 - 1; i >= 0; i--)
{
HeapAdjust(a, i, len-1);//构建最大堆
}
for (int i = len - 1; i > 0; i--)
{
swap(&a[0], &a[i]);//将堆顶记录和最后一个元素交换
HeapAdjust(a, 0, i-1);//将a[0..i-1]重新构建成最大堆
}
}
int main(void)
{
int a[9] = { 50,10,90,30,70,40,80,60,20 };
HeapSort(a, 9);
for (int i = 0; i < 9; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
时间复杂度:O(nlogn),其中建堆O(n),调整O(nlogn),合在一起还是O(nlogn)。
https://www.cnblogs.com/eniac12/p/5329396.html#s3 这个链接讲解了常用的排序算法,还不错。
这两篇文章动图不错:
https://blog.csdn.net/caogenwangbaoqiang/article/details/80191772
https://blog.csdn.net/yushiyi6453/article/details/76407640
快速排序:
快速排序是由Tony Hoare设计的。
1.从序列中挑出一个元素,作为基准(pivot)
2.将小于基准的放在一边,将大于基准的放在另一边
3.重复1,2
这个图演示的跟普通快速排序不一样。
#include<stdio.h>
void swap(int a[], int i, int j)
{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
int Partition(int a[], int left, int right)
{
int pivotkey = a[left];
while (left < right)
{
while (left < right&&pivotkey <= a[right])
right--;
swap(a, left, right);
while (left < right && pivotkey >= a[left])
left++;
swap(a, left, right);
}
return left;
}
//快速排序改进,改进版不需要swap函数
int Partition2(int a[], int left, int right)
{
int pivotkey = a[left];
while (left < right)
{
while (left < right&&pivotkey <= a[right])
right--;
a[left] = a[right];
while (left < right && pivotkey >= a[left])
left++;
a[right] = a[left];
}
a[left] = pivotkey;
return left;
}
void QuickSort(int a[],int left,int right)
{
if (left >= right)
return;
int pivot = Partition2(a, left, right);
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot + 1, right);
}
int main(void)
{
int a[] = { 1,4,7,2,5,8,4,5,7,2,55,23,8 };
QuickSort(a, 0, 12);
for (int i = 0; i <= 12; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
时间复杂度:
一般:O(nlogn)
最坏:O(n^2)
最好:O(nlogn)
优化:基准值的选取,可以在数组中随便算三个数,把中间大小的数作为基准值。