排序:给定一个元素序列,按照每个元素的关键码将元素重新排列,使关键码从小到大(正序)或从大到小(逆序)排列。
排序可根据是否将全部元素放入内存分为内部排序和外部排序。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
这里所说的八大排序就是内部排序。
下面对八大排序算法进行性能比较,包括时间复杂度的最好最坏平均情况,空间复杂度,稳定性。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
下面对每种排序进行详解(均以升序为例)
接口程序:
void Print(int* a, int n); //打印
void InsertSort(int* a, int n); //插入排序
void ShellSort(int* a, int n); // 希尔排序
void SelectSort(int* a, int n); // 选择排序
void AdjustDown(int* a, int n, int root); //重建大堆
void HeapSort(int* a, int n); // 堆排序
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int begin, int end);
//非递归
void QuickSortNonR(int* a, int begin, int end);
//归并
void MergeSrt(int* a, int n);
//归并,非递归
void MergeSrtNonR(int* a, int n);
void CountSort(int* a, int n); //计数排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(相同则插入相等元素后边)。
元素集合越接近有序,直接插入排序算法的时间效率越高。是一种稳定的排序算法。
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1]; //tmp在每循环内不变
while (end >= 0)
{
//int tmp = a[end + 1]; 每次进来就会更新掉tmp达不到目的
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
//a[end + 1] = tmp;
break;
}
}
a[end + 1] = tmp; //还要考虑end<0跳出循环的情况,即tmp比a[0]还要小,所以要在循环外
}
}
希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序。
基本思想是:先选定一个整数,把待排序文件中所有记录分组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
1)选择一个增量序列d[1],t[2],…,d[k],其中d[i]>d[j],d[k]=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序是对直接插入排序的优化。当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
void ShellSort(int* a, int n) // 希尔排序
{
int gap = n;
while (gap > 1) //因为是进入循环后才操作gap,所以最后一次循环必然是 gap == 1.即一次直接插入排序
{
gap = (gap / 5 + 1); //防止gap/5 = 0,所以需要+1,
for (int i = 0; i < n - gap; i++) //直接++就可以回到之前的那一组
{
int end = i;
int tmp = a[end + gap]; //所有地方相当于把直接插入排序的1变成了gap
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。
1)在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3)在剩余的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 (left < right) //当错过了,或相等就结束了
{
int max = left; //都设置为第一个元素,每次换完上下界都要更新
int min = left;
for (int i = left; i <= right; i++)
{
if (a[i] > a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
Swap(&a[min], &a[left]);
if (max == left) //可能存在max == left,此时已经被换走了,所以要找当前的位置,即min
{
max = min;
}
Swap(&a[max], &a[right]);
left++;
right--;
}
}
堆排序是一种树形选择排序,是对直接选择排序的有效改进。利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。需要注意的是排升序要建大堆,排降序建小堆。
算法步骤:
1)创建一个堆H[0…n-1]
2)把堆首(最大值)和堆尾互换
3)把新的数组顶端数据调整到相应位置
4) 重复步骤2,直到堆的尺寸为1
堆排序使用堆来选数,效率就高了很多。
/*
* 堆是数组
n: 数组大小
root: 根位置
*/
void AdjustDown(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++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]); //如果孩子更大,交换
parent = child; //交换后的子树需要重新判定
child = 2 * parent + 1;
}
else //没有发生交换,因为两个子树都是有序的,所以整个树都是有序
{
break;
}
}
}
void HeapSort(int* a, int n) // 堆排序1.建堆 2.排序
{
assert(a);
int j = (n - 2) / 2; //最小的父亲,从最小的父亲开始,-1就是上一个父亲,从此建堆
for (; j >= 0; j--)
{
AdjustDown(a, n, j);
}
int i = n;
while (i--) //排序,0位置和最后一个位置交换
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0); //还剩i个
}
}
根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。**每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。**某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。
冒泡排序是一种非常容易理解的排序。
void BubbleSort(int* a, int n)
{
for (int j = n; j > 0; --j)
{
int change = 0;
for (int i = 0; i < j - 1; i++)
{
if (a[i] > a[i + 1])
{
change = 1;
Swap(&a[i], &a[i + 1]);
}
}
if (change == 0)
{
break;
}
}
}
快速是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。通过排序将序列分割为两部分,左边都是比基线条件小的数,右边都是比它大的数;然后再按照这个方法对分割后的两个序列排序。
将区间按照基准值划分为左右两半部分的常见方式有:
int GetMid(int* a, int left, int right) //防止有序时退化
{
int mid = (left + right) >> 1; //相当于除二
if (a[left] > a[mid])
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[right] > a[mid])
{
return mid;
}
else if (a[right] < a[left])
{
return left;
}
else
{
return right;
}
}
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int midIndex = GetMid(a, left, right);
Swap(&a[left], &a[midIndex]); //把找到的中放在最左侧,不用改算法
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi]) //做key的另一个先走,就一定是比key小的停下来
{
right--;
}
while (left < right && a[left] <= a[keyi]) //left < right &&防止越界
{
left++;
}
Swap(&a[right], &a[left]);
}
Swap(&a[keyi], &a[left]); //交换,把key放在应在的位置
return left;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int midIndex = GetMid(a, left, right);
Swap(&a[left], &a[midIndex]); //把找到的中放在最左侧,不用改算法
int key = a[left]; //存起来
while (left < right)
{
while (left < right && a[right] >= key) //做key的另一个先走,就一定是比key小的停下来
{
right--;
}
a[left] = a[right]; //把小的放在坑里,然后自己变成坑
while (left < right && a[left] <= key) //left < right &&防止越界
{
left++;
}
a[right] = a[left]; //把大的放在坑里,然后自己变成坑
}
a[left] = key;
return left;
}
int PartSort3(int* a, int left, int right) //前后指针
{
int midIndex = GetMid(a, left, right);
Swap(&a[left], &a[midIndex]);
int keyi = left;
int per = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] <= a[keyi] && ++per != cur)
{
Swap(&a[cur], &a[per]);
}
cur++;
}
Swap(&a[keyi], &a[per]);
return per; //返回per,此时per才是本次排序的key
}
void QuickSort(int* a, int begin, int end )
{
if (begin >= end)
{
return;
}
if (end - begin > 20) //小区间优化,小于10时减少递归
{
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1); //左右两个子区间
QuickSort(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1); //注意区间
}
}
//快排非递归
//用栈来保存下来要处理的区间
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin); //先把初始的给进去
StackPush(&st, end);
while (StackEmpty(&st)) //不为空就继续
{
int left, right;
right = StackTop(&st); //将数据取出
StackPop(&st);
left = StackTop(&st); //存先左后右,取时反过来
StackPop(&st);
int keyi = PartSort3(a, left, right);
if (keyi - 1 > left) //左右区间不为零
{
StackPush(&st, left); //key的左侧
StackPush(&st, keyi - 1);
}
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1); //key的右侧
StackPush(&st, right);
}
}
}
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
递归:
void _Merge(int* a, int* tmp, int begin11,int end11, int begin22, int end22) //合并两个部分
{
int begin1 = begin11, end1 = end11;
int begin2 = begin22, end2 = end22;
int i = begin11; //tmp下表
while (begin1 <= end1 && begin2 <= end2) //有一区间走完就完了
{
if (a[begin1] > a[begin2])
tmp[i++] = a[begin2++]; //合并
else
tmp[i++] = a[begin1++];
}
while (begin1 <= end1) //把剩下的也添加到数组
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
for (int j = begin11; j <= end22; j++) //拷贝回原数组,j可以等于RIGHT
{
a[j] = tmp[j];
}
}
void _MergeSrt(int* a,int* tmp, int left, int right)
{
if (left >= right) //最小单位
return;
int mid = (left + right) >> 1; //找中间
_MergeSrt(a, tmp, left, mid); //左右半边排序,mid本身也要处理,不能-1
_MergeSrt(a, tmp, mid + 1, right);
//归并,两个区间的合并
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
_Merge(a, tmp, begin1, end1, begin2, end2);
}
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void MergeSrt(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
exit(-1);
}
_MergeSrt(a, tmp, 0, n - 1);
free(tmp);
}
非递归
//归并,非递归
void MergeSrtNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
exit(-1);
}
int gap = 1;
while (gap < n) //从1开始,到n,不断地合并分组,进行归并,递归的下半部分。
{
for (int i = 0;i<n ;i += 2 * gap) //找不同的小组
{
int begin1 = i, end1 = i + gap - 1; //第一个区间,i- (i+gap-1)
int begin2 = i + gap, end2 = i + 2 * gap - 1; //第二个区间i + gap- i + 2 * gap - 1(i + gap+gap-1)
//所以第二次gap直接乘二,因为每2gap内都不需要再拍
if (begin2 >= n) //1.第一个区间就不够了,2.第二个区间不存在 3.第二个区间不玩整 , 1和2的情况一样,1不完整自然2就不存在,且只存在有序的1或不完整的有序1,都是不需要进行继续排序的。
//3.的情况需要将不完整的第二区间进行归并。
{
break; //数组的最后。也就是这个GAP最后的一次循环,所以直接跳出即可
}
if (end2 >= n)
{
end2 = n - 1; //修正
}
_Merge(a, tmp, begin1, end1, begin2, end2);
}
gap *= 2; //每次是之前的2倍,11一组,22一组,44一组,这样每组内部就是排好序的
}
free(tmp);
}
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。操作步骤:
void CountSort(int* a, int n) //计数排序
{
int max = a[0], min = a[0]; //相对映射
int i = 0;
for (i = 0 ; i < n; i++)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
int range = max - min + 1; //范围
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range); //初始化
for (int k = 0; k < n; k++)
{
count[a[k] - min]++; //相对位置加1
}
i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--) //同一个位置的n次
{
a[i++] = j + min;
}
}
free(count);
}
谢谢大家