1.交换排序
交换排序基本思想: 所谓交换,就是根据序列中两个记录键值的⽐较结果来对换这两个记录在序列中的位置 交换排序的特点是:将键值较⼤的记录向序列的尾部移动,键值较⼩的记录向序列的前部移动。
1.1冒泡排序
例子:
int arr[] = { 3,1,6,5,2,7,9,8,4 };
基本思想:
冒泡排序就是将数组中的元素一一比较,然后交换位置,直到超出数组范围。一共n个数据,需要遍历n-1次,每遍历一次将其一一比较,遍历完一次就可以确定一个数据位置,所以每遍历完一次,下一次遍历的结束条件就要减一。
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 0;
}
}
if (flag == 1)
break;
}
}
这样一次遍历就将9排到指定位置,下一次遍历到9前面那个位置就可以了,所以每次遍历完下一次截止条件j-1-i,直到剩余第一个元素;这里有一个优化处理,定义flag为1,如果没有进入第二层循环,前一个数据都小于后一个了,说明已经有序了,就不用排了,直接跳出循环。冒泡排序时间复杂度O(n^2)。
1.2快速排序
快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法,其基本思想为:任取待排序元素 序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩ 于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列 在相应位置上为⽌。
快速排序实现主框架:
//快速排序
void QuickSort(int*a,int left,int right)
{
if(left >= right) {
return;
}
//_QuickSort⽤于按照基准值将区间[left,right)中的元素进⾏划分
int keyi= _QuickSort(a, left, right);
QuickSort(a, left, meet - 1);
QuickSort(a, meet + 1, right);
}
将区间中的元素进⾏划分的 _QuickSort ⽅法主要有以下⼏种实现⽅式:
1.2.1 hoare版本
算法思路:创建左右指针,确定基准值;从右向左找出⽐基准值⼩的数据,从左向右找出⽐基准值⼤的数据,左右指针数据交换,进⼊下次循环;
int _QuickSort1(int* a, int right, int left)
{
int keyi = left;
++left;
while (left <= right)
{
while(left <= right && a[right] > a[keyi])
right--;
while(left <= right && a[left] < a[keyi])
left++;
if (left <= right)
Swap(&a[left++], &a[right--]);
}
Swap(&a[keyi], &a[right]);
return right;
}
注意:这里判断条件中,当a[keyi]=a[left];或者=a[right],不需要进行交换,因为当数据中有大量相等的数据是算法的效率会降低。
1.2.2挖坑法
思路:
创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新 的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结 束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)
// 快速排序挖坑法
int _QuickSort2(int* a, int right, int left)
{
int hole = left;
int key = a[hole];
while (left < right)
{
while (left<right && a[right]>key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left<right && a[left]<key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
1.2.3 lomuto前后指针
创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。
//双指针法
int _QuickSort3(int* a, int right, int left)
{
int prev = left, cur = prev + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
时间复杂度:外层每一次相当于二分logn,内层遍历为n,所以时间复杂度为 O(nlogn)。
1.3⾮递归版本
⾮递归版本的快速排序需要借助数据结构:栈。(这里小编前面栈文章里面有详细讲解)
void QuickSortNonR(int* a, int left, int right)
{
ST s;
StackInit(&s);
StackPush(&s, right);
StackPush(&s, left);
while (!StackEmpty(&s))
{
int begin = StackTop(&s);
StackPop(&s);
int end = StackTop(&s);
StackPop(&s);
int keyi=_QuickSort3(a, end, begin);
if (keyi + 1 < end)
{
StackPush(&s, end);
StackPush(&s, keyi+1);
}
if (keyi - 1 > begin)
{
StackPush(&s, keyi-1);
StackPush(&s, begin);
}
}
StackDestory(&s);
}
先将左右位置,入栈,先将右端入栈,左再入栈,进循环后去两次栈顶,利用取出来的两个位置,去找基准值,再将基准值,右边部分的两端点入栈,和左边两端点入栈,循环直到栈为空。
2. 插⼊排序
基本思想:直接插⼊排序是⼀种简单的插⼊排序法,其基本思想是:把待排序的记录按其关键码值的⼤⼩逐个插 ⼊到⼀个已经排好序的有序序列中,直到所有的记录插⼊完为⽌,得到⼀个新的有序序列。
2.1直接插⼊排序
当插⼊第 i(i>=1) 个元素时,前⾯的⽤ array[0],array[1],…,array[i-1] 已经排好序,此时 array[i] 的排序码与 即将 array[i-1],array[i-2],…的排序码顺序进⾏⽐较,找到插⼊位置 array[i] 插⼊,原来位置上的元素顺序后移。
void InsertSort(int* a, int n)
{
for (int i = 0; i < n-1; i++)
{
int end = i;
int temp = a[i+1];
while (end >=0)
{
if (a[end] > temp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = temp;
}
}
每次将end后面一个数,与end及之前的一一比较,当end位置的值大于tmp,就将end位置的值,向后移动,找到小于tmp的位置后就将tmp赋值给这个位置,完成一次循环,直到end走到n-1位置就完成排序了。
时间复杂度为O(n^2),实际上只有将数组为降序时,再排成升序,这种情况,才会达到O(n^2),实际运行效率是要低于O(n^2)。
2.2希尔排序(直接插入排序的优化)
希尔排序法⼜称缩⼩增量法。希尔排序法的基本思想是:
先选定⼀个整数(通常是gap=n/3+1),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进⾏排 序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序。 它是在直接插⼊排序算法的基础上进⾏改进⽽来的,综合来说它的效率肯定是要⾼于直接插⼊排序算法的。
//希尔排序(直接插入排序的优化)
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int temp = a[i + gap];
while (end >= 0)
{
if (a[end] > temp)
{
a[end + gap] = a[end];
end-=gap;
}
else
{
break;
}
}
a[end + gap] = temp;
}
}
}
这里gap每次划分也要有一定规划,例如gap/2划分的组多,每次比较的次数少,gap/8划分的组少,每组要比较的次数较多;一般取值gap/3。
希尔排序是对直接插⼊排序的优化。 2. 当 gap > 1 时都是预排序,⽬的是让数组更接近于有序。当gap == 1,数组已经接近有序的了,这样就会很快。这样整体⽽⾔,可以达到优化的效果。
希尔排序的时间复杂度估算:外层循环的时间复杂度可以直接给出为:即 O(log2n) 或者O(log3 n) ,O(logn),内层循环:假设⼀共有n个数据,合计gap组,则每组为n/gap个;在每组中,插⼊移动的次数最坏的情况下为 1 +2+3+....+( n/gap-1) ,⼀共是gap组,因此:
总计最坏情况下移动总数为:gap*[1 +2+3+....+( n/gap-1))];
gap取值有(以除3为例):n/3 n/9 n/27 ...... 2 1
当gap为n/3时,移动总数为:n/3*(1+2)=n;
当gap为n/9时,移动总数为:n/9*(1+2+3+....+8)=4n;
最后⼀躺,gap=1即直接插⼊排序,内层循环排序消耗为n
可以得出以下曲线:
因此,希尔排序在最初和最后的排序的次数都为n,即前⼀阶段排序次数是逐渐上升的状态,当到达 某⼀顶点时,排序次数逐渐下降⾄n,⽽该顶点的计算暂时⽆法给出具体的计算过程,大概约等于n^1.3。
3.选择排序
选择排序的基本思想:
每⼀次从待排序的数据元素中选出最⼩(或最⼤)的⼀个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
3.1直接选择排序
在元素集合 array[i]--array[n-1] 中选择关键码最⼤(⼩)的数据元素,若它不是这组元素中的最后⼀个(第⼀个)元素,则将它与这组元素中的最后⼀个(第⼀个)元素交换,在剩余的 array[i]--array[n-2] ( array[i+1]--array[n-1] ) 集合中,重复上述步 骤,直到集合剩余1个元素。
//选择排序
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (right > left)
{
int min = left;
int max = left;
for (int i = left + 1; i < right; i++)
{
if (a[min] > a[i])
{
min = i;
}
if (a[max] < a[i])
{
max = i;
}
}
if (max == left)
max = min;
Swap(&a[left], &a[min]);
Swap(&a[right], &a[max]);
right--;
left++;
}
}
注意:这里当第一个元素是max是第一个元素时,min先于第一个元素交换后就找不到max了
,所以这里要进行特殊处理一下,将max位置放到min位置。
时间复杂度:两层循环,内层开起来是找到max,min,是两次,但其实只循环了一次,所以时间复杂度O(n^2)。
3.2堆排序
堆排序(Heapsort)是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀ 种。它是通过堆来进⾏选择数据。需要注意的是排升序要建⼤堆,排降序建⼩堆。(基本思想在小编前面的文章,堆排序和Top—K中有详细讲述)
//堆排序
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = 2 * parent+1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child += 1;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent+1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
int parent = (n-1-1 )/2;
while (parent >= 0)
{
AdjustDwon(a, n, parent);
parent--;
}
int size = n - 1;
while (size > 0)
{
Swap(&a[0], &a[size]);
AdjustDwon(a, size, 0);
size--;
}
}
4.归并排序
归并排序算法思想:
归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide andConquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个 ⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。归并排序核心步骤。
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = left+(right-left) / 2;
_MergeSort(a, left, mid , tmp);
_MergeSort(a, mid+1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
for (int i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
void MergeSort(int* a, int n)
{
int* tmp=(int*)malloc(sizeof(int) * n);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
算法时间复杂度:外层是一个相当于二分,即logn,内层是一个遍历n,所以算法时间复杂度为O(nlogn)。
5.排序性能比较
这里通过排十万组数据,可以大概观察到各排序算法的效率。
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, N - 1,0 );
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
BubbleSort(a7, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}