文章目录
常见排序算法的实现
1.插入排序
思想: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列.
实际中我们玩扑克牌时,就用了插入排序的思想
步骤: 由于刚开始不确定数组中前多少个元素是有序的,所以我们可以看成数组中只有第一个元素有序,从第二个元素开始进行插入排序,直到所有元素插入完成,则排序完成。
动图演示:
代码:
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
//记录有序数组中最后一个元素的下标
int end = i-1;
//当前要排序的数字
int tmp=a[i];
//将tmp插入到[0,end]区间,保持有序
while (end >= 0)
{
if (tmp < a[end])
{
//如果tmp比有序数组中最后一个元素还要小,则继续跟前面的比较
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
//两种情况 ①tmp小于数组中所有的元素,end<0 ②tmp>a[end]
a[end + 1] = tmp;
}
}
2.希尔排序
希尔排序法又称缩小增量法。
思想: 先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。
当到达=1时,所有记录在统一组内排好序。
希尔排序,先将待排序列进行预排序,使待排序列接近有序,然后再对该序列进行一次插入排序(当gap=1的时候,就为一次插入排序),此时插入排序的时间复杂度为O(N),
动图演示:
代码:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap =gap/2;
//将数组分为gap组,进行排序
for (int j = 0; j < gap; j++)
{
//单趟排序
for (int i = gap + j; i < n; i += gap)
{
//跟插入排序思路一样,记录数组第一个元素的下标
int end = i - gap;
//记录要进行排序的数字
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else {
break;
}
}
//两种情况 ①tmp小于数组中所有的元素,end<0 ②tmp>a[end]
a[end + gap] = tmp;
}
}
}
}
3.选择排序
思路: 从待排序的数组中遍历一遍,找出最大值(最小值)存放在序列的起始(末尾)位置,也可以遍历一次找出最大值和最小值,然后放到数组两侧,这样就可以很快排序出数组
动图演示:
代码:
这段代码一次遍历选出最大值和最小值,然后将最小值和起始位置交换位置,最大值和末尾位置交换位置,注意:在交换最小值后需判断是不是把找到的最大值给换走了,防止出现排序错误。
void SelectSort(int* a, int n)
{
int left = 0;
int right = n-1;
while (left < right)
{
int min = left, max = left;
for (int i = left+1; i <= right; i++)
{
if (a[min] > a[i])min = i;
if (a[max] < a[i])max = i;
}
swap(&a[min], &a[left]);
//判断一下最大值是不是left,如果是更新一下下标,防止left与最小值交换值的时候把最大值换掉
if (max==left)
{
max = min;
}
swap(&a[max], &a[right]);
left++;
right--;
}
}
4.堆排序
1.建堆
- 排升序,建大堆
- 排降序,建小堆
2.堆排序
这里升序为例,先将数组内容建成一个大堆,利用堆删除的思想来进行堆排序,
我们知道,大堆的堆顶为堆中最大的数,我们可以将堆顶的元素和堆底交换一下,这样我们堆底就为堆中最大的元素,剩下的元素堆顶进行向下调整,这样我们可以选出堆中次大的元素,重复此步骤
动图演示:
代码:
void HeapSort(int* a, int n)
{
//建堆 -- 向下调整建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
//时间复杂度:O(N*LogN)
//交换最后一个元素和堆顶元素
swap(&a[end], &a[0]);
//向下调整,选出最大的元素当堆顶
AdjustDown(a, end, 0);
--end;
}
}
void HeapSort(int* a, int n)
{
//建堆 -- 向下调整建堆 O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
//时间复杂度:O(N*LogN)
//交换最后一个元素和堆顶元素
swap(&a[end], &a[0]);
//向下调整,选出最大的元素当堆顶
AdjustDown(a, end, 0);
--end;
}
}
5.冒泡排序
思路: 冒泡排序是一种比较经典的排序,具有不错的教学意义,他的思想也特别的简单,数组内两两元素进行比较,(升序)前一个元素比后一个元素大则交换位置,一趟排序会排出一个最大(最小)的数。
动图演示:
代码:
思路:
void ButtleSort(int* a, int n)
{
for (int j=0;j<n;j++)
{
bool exchange = true;
for (int i = 1; i < n-j; i++)
{
if (a[i - 1] > a[i])
{
swap(&a[i - 1], &a[i]);
exchange = false;
}
}
//优化:如果数组已经有序,则直接跳出循环
if (exchange)break;
}
}
6.快速排序
① Hoare
思路:
1. 选出一个基准值key,一般选最左边或者最右边
2.如果待排序数组中的值接近有序,那么快速排序的时间复杂度可能会达到O(N^2),为避免出现这种特殊情况,使用三数取中,即选数组最边、最右、中间数进行比较,返回三数中的中间值,则可避免出现的特殊情况。
3.定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走,这样可以保证他们相遇位置元素永远小于key位置的元素;若选择最右边的数据作为key,则需要bengin先走)。
4.在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。
5.此时key的左边都是小于key的数,key的右边都是大于key的数
6.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序
单趟排序动图演示:
代码:
//Hoare
int PartSort1(int* a, int left, int right)
{
//三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)swap(&a[midi], &a[left]);
int keyi = left;
while (left < right)
{
//排升序
//从右边找比key位置小的数
while (right > left && a[right] >= a[keyi])
{
right--;
}
//从左边开始找比key位置大的数
while (left < right && a[left] <= a[keyi])
{
left++;
}
swap(&a[left], &a[right]);
}
swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)return;
if((right-left+1)>10)
{
//调用完此次单趟排序后,keyi左边都为比keyi小的值,keyi右边都为比keyi大的值
int keyi = PartSort1(a, left, right);
//将keyi左边再次进行排序
QuickSort(a, left, keyi - 1);
//将keyi右边再次进行排序
QuickSort(a, keyi + 1, right);
}else{
InsertSort(a+left,right-left+1);
}
}
②挖坑法
思路:
1.先将选定的基准值(最左边)直接取出,然后留下一个坑,
2.当右指针遇到小于基准值的数时,直接将该值放入坑中,而右指针指向的位置形成新的坑位,
3.然后左指针遇到大于基准值的数时,将该值放入坑中,左指针指向的位置形成坑位,
4.重复该步骤,直到左右指针相等。最后将基准值放入坑位之中。
单趟排序动图演示:
代码:
int PartSort2(int* a, int left, int right)
{
//三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)swap(&a[midi], &a[left]);
//坑
//key存储坑位数据
int key = a[left];
int hole = left;
while (left < right)
{
while (right > left && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//从左边开始找比key位置大的数
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)return;
if((right-left+1)>10)
{
//调用完此次单趟排序后,keyi左边都为比keyi小的值,keyi右边都为比keyi大的值
int keyi = PartSort1(a, left, right);
//将keyi左边再次进行排序
QuickSort(a, left, keyi - 1);
//将keyi右边再次进行排序
QuickSort(a, keyi + 1, right);
}else{
InsertSort(a+left,right-left+1);
}
}
③ 前后指针法
思路:
1.选定基准值,定义prev和cur指针(cur = prev + 1)
2.cur先走,遇到小于基准值的数停下,然后将prev向后移动一个位置
3.将prev对应值与cur对应值交换
4.重复上面的步骤,直到cur走出数组范围
5.最后将基准值与prev对应位置交换
6.递归排序以基准值为界限的左右区间
排序动图演示:
单趟演示:
代码:
//前后指针法
int PartSort3(int* a, int left, int right)
{
//三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)swap(&a[midi], &a[left]);
//存下标
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] >= a[keyi])cur++;
else if (a[cur] < a[keyi])swap(&a[++prev], &a[cur++]);
}
swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)return;
if((right-left+1)>10)
{
//调用完此次单趟排序后,keyi左边都为比keyi小的值,keyi右边都为比keyi大的值
int keyi = PartSort1(a, left, right);
//将keyi左边再次进行排序
QuickSort(a, left, keyi - 1);
//将keyi右边再次进行排序
QuickSort(a, keyi + 1, right);
}else{
InsertSort(a+left,right-left+1);
}
}
④非递归
思路: 快速排序非递归实现,需要借助栈,栈中存放的是需要排序的左右区间。而且非递归可以彻底解决栈溢出的问题
步骤:
1.将数组左右下标入栈,
2.若栈不为空,两次取出栈顶元素,分别为闭区间的左右界限
3.将区间中的元素按照前后指针法排序(其余两种也可)得到基准值的位置
4.再以基准值为界限,若基准值左右区间中有元素,则将区间入栈
5.重复上述步骤直到栈为空
代码:
void QuickSortNonR(int* a, int left, int right)
{
ST st;
STInit(&st);
//先把要排序区间压栈
STPush(&st,right);
STPush(&st,left);
while (!STEmpty(&st))
{
//如果right先入栈,栈顶就为left
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = PartSort3(a, begin, end);
//判断一下还需不需要排序
if (keyi+1 < end)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestory(&st);
}
7.归并排序
思路: 归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
排序动图演示:
① 递归
代码:
void MargeSort(int* a, int n)
{
//归并排序一般需要临时申请一段空间
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MargeSort(a, 0,n-1,tmp);
//用完临时空间记得释放
free(tmp);
}
void _MargeSort(int* a, int begin, int end,int* tmp)
{
if (begin>=end)return;
int mid = (begin + end) / 2;
//递归分治到最小单位
_MargeSort(a, begin, mid, tmp);
_MargeSort(a, mid+1, end, tmp);
//合并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1,end2 = end;
//此处i不能为0
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,sizeof(int)*(end-begin+1));
}
②非递归
非递归实现的思想与递归实现的思想是类似的。
不同的是,这里的序列划分过程和递归是相反的,不是一次一分为二,而是先1个元素一组,再2个元素一组,4个元素一组…直到将所有的元素归并完。
这里的gap表示每次归并每组的元素个数,
代码:
//非递归
void MargeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
//表示归并的元素个数
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//当原数组中元素个数不是2^n时,最后两组组会出现元素不匹配的情况
//[begin1,end1][begin2,end2]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//修正
//情况一 end1越界,此时需要调整end1为数组最后一个元素位置,从begin1到数组最后一个元素进行归并
// 使begin2到end2不进行归并需要重新赋值一下使其不满足归并条件
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
//情况二 begin2越界,使begin2到end2不进行归并需要重新赋值一下使其不满足归并条件
else if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
//情况三 end2越界,时需要调整end2为数组最后一个元素位置
else if (end2 >= n)
{
end2 = n - 1;
}
//
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//将tmp中的数据拷贝回a数组
memcpy(a, tmp,sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
8.计数排序
思路: 计数排序是一个非基于比较的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法。
排序的步骤:
1.根据待排序集合中最大元素和最小元素的差值范围,申请额外空间;
2.遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内;
3.对额外空间内数据进行计算,得出每一个元素的正确位置;
4.将待排序集合每一个元素移动到计算得出的正确位置上。
排序动图演示:
代码:
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)max = a[i];
if (a[i] < min)min = a[i];
}
int range = max - min + 1;
int* countA = malloc(sizeof(int) * range);
if (countA == NULL)
{
perror("malloc fail");
return;
}
memset(countA, 0, sizeof(int)*range);
//计数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
free(countA);
}
注意: 计数排序适合范围集中,且范围不大的整形数组排序,不适合范围分散或者非整形的排序,如字符串,浮点数等
排序算法的复杂度及稳定性分
稳定性:指数组中相同元素在排序后相对位置不发生变化。
冒泡排序中,相同元素比较不会交换位置,从而排序后相同元素的相对位置不会发生变化,所以冒泡排序相对稳定,
插入排序中,一个数要插入到有序数组中,那么在这个有序数组中如果碰到跟要插入元素相同的元素只会插入到其后面,不会改变相对位置。
希尔排序分组后,可能会改变元素的相对位置,所以不稳定。
快速排序基准值两边的元素会进行交换,也会导致排序不稳定。
归并排序 合并数据时也不会使两个相等的元素相对位置发生变化,所以稳定。