排序在实现生活上可谓是不可缺少的一部分,比如我们网上购物,会按照某一个属性进行排序访问,
再比如平时查看大学排名等等都需要排序
目的就是方便我们筛选,选择
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为
止,得到一个新的有序序列
实际中我们玩扑克牌时,就用了插入排序的思想
当插入第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 tem = a[end + 1];//保存后一个值
while (end >= 0)
{
if (tem < a[end])//后一个值小,前面的值往后移
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tem;//把小的值插到属于它的位置
}
}
直接插入排序的特性总结:
直接插入排序在时间复杂度为O(N^2)的排序上算得上是佼佼者了,因为它最好的情况上排序的时间复杂度为O(N),就是当它排一个有序的数据时,它是比较高效率的,甚至比快速排序还快(当然前提是数据有序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个
组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后分别进行插入排序,重复上述分组和排序的工
作。当到达gap=1时,所有记录在统一组内排好序。
选定的那个整数,就是gap,是希尔排序的核心,至于怎么选,都是有技巧的!
一些动画的演示:
// 希尔排序
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 tem = a[end + gap];//保存第后gap的值
while (end >= 0)
{
if (tem < a[end])//后gap的值小,前面的值往后移
{
a[end + gap] = a[end];
end-= gap;
}
else
{
break;
}
}
a[end + gap] = tem;//把小的值插到属于它的位置
}
}
}
希尔排序的特性总结:
《数据结构(C语言版)》— 严蔚敏
你是否有过这样的疑惑?
既然希尔排序是插入排序的一种优化,那这优化力度有多大呢,感觉也就那样!
其实并非这样,希尔排序的优化,可谓是给插入排序如虎添翼了,实践是检验真理的唯一标准,下面我们用实际数据来检验一下它的优化之牛
我们用十万个随机数分别让插入排序和希尔排序分别排序,计算它们运行的时间,可以发现它们两的差距还是很大的,说明希尔排序的优化还是很牛的(单位是毫秒)
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的
数据元素排完
选择排序一趟选最小的放前面,我们可以稍微优化一下,在一趟中同时选出最大和最小值,分别放在前面和后面
代码实现:
// 选择排序
void SelectSort(int* a, int n)
{
int begin = 0, end = n-1 ;
while (begin < end)
{
int maxi = begin, mini = begin;
//选数,同时选出最大的和最小的
for (int i = begin; i <= end; i++)
{
if (a[maxi] < a[i])//选出大的那个下标
maxi = i;
if (a[mini] > a[i])//选出小的那个下标
mini = i;
}
Swap(&a[mini], &a[begin]);
//如果刚好最大值在begin位置,则要更新maxi
if (maxi == begin)
maxi = mini;
Swap(&a[maxi], &a[end]);
begin++;
end--;
}
}
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是
通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
✨【二叉树】数中的特殊结构->堆✨
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
void Swap(int* e1, int* e2)
{
int tem = *e1;
*e1 = *e2;
*e2 = tem;
}
//向下调整算法
void AdjustDwon(int* a, int n, int parent)
{
int child = parent * 2 + 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 = parent * 2 + 1;
}
else
{
break;
}
}
}
// 堆排序
void HeapSort(int* a, int n)
{
//建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
//排序
for (int i = 0; i < n; i++)
{
Swap(&a[0], &a[n-i-1]);//把大的放到后面
AdjustDwon(a, n - i-1, 0);//重新调整堆
}
}
堆排序的特性总结:
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序的原理是:从左到右,相邻元素进行比较。每次比较一轮,就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。
以从小到大排序为例,第一轮比较后,所有数中最大的那个数就会浮到最右边;第二轮比较后,所有数中第二大的那个数就会浮到倒数第二个位置……就这样一轮一轮地比较,最后实现从小到大排序。
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 0;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 1;//标志本趟有交换元素
}
}
//在一趟比较中,如果没有发生交换元素,说明已经有序,不需要再排序
if (flag == 0)
break;
}
}
冒泡排序的特性总结:
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中
的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右
子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉
树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可
将区间按照基准值划分为左右两半部分的常见方式有:
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = 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[left], &a[keyi]);
return left;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort1(a, left, right);
//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);//递归左子区间
QuickSort(a, pivot+1, right);//递归右子区间
}
一趟的排序动画演示
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];//保存基准值
int hole = left;//坑位
while (left < right)
{
//找小
while (left < right && a[right] >= key)
{
right--;
}
//放坑位
if (left < right)
{
a[hole] = a[right];
hole = right;//更新坑位
}
//找大
while (left < right && a[left] <= key)
{
left++;
}
if (left < right)
{
a[hole] = a[left];
hole = left;
}
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort2(a, left, right);
//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);//递归左子区间
QuickSort(a, pivot+1, right);//递归右子区间
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = left+1;
while (cur <= right)
{
//cur从左往右找小跟prev换
if (a[cur] < a[keyi] && ++prev!=cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort3(a, left, right);
//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);//递归左子区间
QuickSort(a, pivot+1, right);//递归右子区间
}
快速排序为什么还需要优化呢?
快速排序在选key的时候,最理想,效率最高的情况就是每次都能选到中间值
但是,快速排序在没有优化前,对数据有序的情况进行排序,那么它每次选key的值都在最左边或最右边,效率就会大大降低,
下面是我用十万个有序数据对快速排序优化前后测出来的数据对比(单位ms)
三数取中讲究的就是让我们每次选key的时候都能保证它大概中分区间,不至于每次都选到最大或最小。
代码实现:
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[left] > a[mid])
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
else//a[left] <= a[mid]
{
if (a[left] > a[right])
return left;
else if (a[right] > a[mid])
return mid;
else
return right;
}
}
用三数取中优化后的各个版本的快排
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);//取相对中间的值
Swap(&a[mid], &a[left]);//跟第一个值进行交换,下面逻辑就不用变化太大,下面版本也类似
int keyi = 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[left], &a[keyi]);
return left;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
int key = a[left];//保存基准值
int hole = left;//坑位
while (left < right)
{
//找小
while (left < right && a[right] >= key)
{
right--;
}
//放坑位
if (left < right)
{
a[hole] = a[right];
hole = right;//更新坑位
}
//找大
while (left < right && a[left] <= key)
{
left++;
}
if (left < right)
{
a[hole] = a[left];
hole = left;
}
}
a[hole] = key;
return hole;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
int keyi = left;
int prev = left;
int cur = left+1;
while (cur <= right)
{
//cur从左往右找小跟prev换
if (a[cur] < a[keyi] && ++prev!=cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort2(a, left, right);
//分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot+1, right);
}
其实快速排序经过上面的三数优化之后,大体上已经没有很多问题,也不会出现最坏的情况,但是还可以进行优化
.
因为快排在实现排序的时候,当区间已经变小了,那么它的数据就相对已经有序了很多,此时我们就可以使用插入排序对它的小区间进行排序,插入排序对相对有序的数据进行排序效率是比较高的。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort2(a, left, right);
//分成了两个区域 [left,pivot] pivot [pivot+1,right];
//小区间优化
if (right - left < 8)//当区间比较小的时候就可以使用插入排序
{
InsertSort(a + left, right - left + 1);
}
else
{
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot + 1, right);
}
}
递归改循环,有两种情况
快速排序的非递归需要借助数据结构的栈去模拟递归的实现
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
int pivot = PartSort2(a, begin, end);
//[begin, pivot-1] pivot [pivot+1,end]
//当区间有数才入栈
if (end - pivot > 1)
{
StackPush(&st, end);
StackPush(&st, pivot + 1);
}
if (pivot - begin > 1)
{
StackPush(&st, pivot - 1);
StackPush(&st, begin);
}
}
}
//小区间优化版本
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
//小区间优化
if (end - begin < 8)
{
InsertSort(a + begin, end - begin + 1);
continue;
}
int pivot = PartSort2(a, begin, end);
//分出的区间:[begin, pivot-1] pivot [pivot+1,end]
//当区间有数才入栈
if (end - pivot > 1)
{
StackPush(&st, end);
StackPush(&st, pivot + 1);
}
if (pivot - begin > 1)
{
StackPush(&st, pivot - 1);
StackPush(&st, begin);
}
}
}
快速排序的特性总结:
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
//取中间数
int mid = begin + (end - begin) / 2;
//递归子问题
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//合并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
//取小的先放
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//把其中一个没有取完的数组进行取数
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//拷贝回原数组
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
//开辟辅助空间
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
归并排序的非递归可以直接改循环处理
但是在边界处理的时候需要及其小心
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
//gap控制每组有多少个元素进行归并
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
//合并
int begin1 = j, end1 = j+gap-1;
int begin2 = j+gap, end2 = j+2*gap-1;
int i = j;
//end1越界
if (end1 >= n)
{
break;
}
//begin2 这组全部越界
if (begin2 >= n)
{
break;
}
//end2越界
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//拷贝回去(归那一部分就拷贝那一部分)
memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
}
gap *= 2;
}
}
归并排序的特性总结:
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
// 计数排序
void CountSort(int* a, int n)
{
int max, min;
max = min = a[0];
//找出最大最小值
for (int i = 0; i < n; ++i)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
//开辟对应大小的空间
int* count = (int*)malloc(sizeof(int) * (max - min + 1));
if (count == NULL)
{
perror("malloc fail\n");
return;
}
//将辅助数组中的值初始化成0
memset(count, 0, sizeof(int) * (max - min + 1));
//统计每个数出现的次数
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
//开始拷贝回原数组
int i = 0, j = 0;
while (j < n)
{
if (count[i] == 0)
{
i++;
continue;
}
a[j++] = i + min;
count[i]--;
}
}
计数排序的特性总结:
排序算法有很多,比如还有基数排序,桶排序…但是我们掌握上面这八种足矣!
毕竟算得上经典的也就这么多,比较常用的也包括在里面了!
下面是一些排序的实测数据结果(冒泡,非比较排序不在里面)单位ms
一共是一百万个随机数
可以看出仅仅一百万个数据插入排序和选择排序的速度是非常缓慢的。