如果我们从小就开始编写程序,那么也许长大以后我们自然而然地就能够阅读它们了。
void InsertSort(int* a, int n)
{
//end最后一个位置是n-2,也就是倒数第二个数
for (int i = 0; i < n - 1; ++i)
{
int end = i;
// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持他依旧有序
int tmp = a[end + 1];
while (end >= 0)
{
//如果排降序,那么此处改为>
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
关于最后一条语句:a[end + 1] = tmp;
插入数据可能是两种情况:
有n个数据就要比较n-1趟,第一趟比较的次数为n-1-1,以后每趟的比较次数依次减1(因为每次都把最大的数据沉到最后),且如果有一趟的比较时没有发生交换,就表明数据已经有序,此时就跳出循环。
#include
void BubbleSort(int* a, int size)
{
for (int i = 0; i < size - 1; ++i)
{
int exchange = 0;
for (int j = 1; j < size - i; ++j)
{
if (a[j - 1] > a[j])
{
int temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
希尔排序是插入排序的一种,又称缩小增量排序。是改进版的插入排序
传统写法:
void ShellSort(int* a, int n)
{
//1. gap>1 预排序
//2. gap==1 直接插入排序
//3. 我们要保证最后一次gap==1
int gap = n;
while(gap > 1)
{
//gap/=2最后一定为1
//gap /= 2;
//当1
gap = gap/3+1;
//控制组数
for (int j = 0; j < gap; ++j)
{
//控制每一组进行直接插入排序
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
//同样有两种情况
//tmp比a[end]大,break跳出循环插入
// end走到-1进入不了循环时插入
a[end + gap] = tmp;
}
}
}
}
简化写法:
我们不用外层的for循环控制组数,把第二层for循环i+=gap改为++i.表示多组同时进行直接插入排序。
void ShellSort(int* a, int n)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
直接选择排序也称简单选择排序,是一种相对简单的排序算法,它的基本思想是:从一列数中找出最小的,和第一个交换;剩下的重新找出最小的,和这列数的第二个交换,…一直进行n-1次比较之后,该数列已经为有序数列了。
例如:已知一组无序数列:6 3 5 1 4 2 9
第一次:[6 3 5 1 4 2 9] 最小数为:1
第二次:1 [3 5 6 4 2 9] 最小数为:2
第三次:1 2 [5 6 4 3 9] 最小数为:3
第四次:1 2 3 [6 4 5 9] 最小数为:4
第五次:1 2 3 4 [6 5 9] 最小数为:5
第六次:1 2 3 4 5 [6 9] 最小数为:6
第七次:1 2 3 4 5 6 [9] 已经为有序数列了。
我们这里一次选两个数,选出最小值放在第一个,最大值放在最后一个。再选出次小的值放在第二个,次大值放在倒数第二个,依次迭代往后走。
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int mini = left, maxi = left;
for (int i = left + 1; i <= right; ++i)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[left], &a[mini]);
// 如果left和maxi重叠,修正一下maxi即可
if (left == maxi)
maxi = mini;
Swap(&a[right], &a[maxi]);
left++;
right--;
}
}
当maxi和left重叠时,执行语句Swap(&a[left],&a[mini])后,maxi就被换到了mini的位置。
解决方法:
此时只要修正一下maxi即可
if (left == maxi)
maxi = mini;
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
之前详细介绍过堆和堆排序
博客链接
void Swap(HPDataType* pa, HPDataType* pb)
{
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
// 1、选出左右孩子中大的那个
if (child + 1 < size && a[child + 1] > a[child])
{
++child;
}
// 2、如果孩子大于父亲,则交换,并继续往下调整
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 - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
size_t end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
// 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;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
相比hoare版本,挖坑法更好理解,但本质上没有区别。
不需要理解为什么最终相遇位置比key小!
不需要理解为什么左边做key,右边先走!
代码演示:
// 挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
// 坑位
int pit = left;
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= key)
{
--right;
}
a[pit] = a[right];
pit = right;
// 左边走,找大
while (left < right && a[left] <= key)
{
++left;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
prev和cur的关系:
代码实现:
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
关于a[++prev] != a[cur]
有两个作用:
void QuickSort(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
int keyi = PartSort(a, begin, end);//上述三种方法选1即可
// [begin, keyi-1]keyi[keyi+1, end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
为什么要优化,快排存在着一种最坏情况:
优化方法:
代码演示:
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
// left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
//此时mid最大,剩下两个谁大谁就是中间的值
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
//此时mid是最小的,剩下两个谁小谁就是中间的值
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
// 前后指针法
int PartSort3(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && a[++prev] != a[cur])
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
int keyi = PartSort(a, begin, end);//上述三种方法选1即可
// [begin, keyi-1]keyi[keyi+1, end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
void QuickSort2(int* a, int begin, int end)
{
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
return;
// 小区间直接插入排序控制有序
if (end - begin + 1 <= 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1]keyi[keyi+1, end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
我们发现快速排序递归版本,递归调用时建立栈帧实际上是在存储要处理的区间。那我们也可以用一个栈来存储区间来模拟递归调用。C语言没有栈,我们可以把之前写的Stack.c,Stack.h文件复制到现有项。
代码实现:
#include "Stack.h"
// 非递归
void QuickSort3(int* a, int begin, int end)
{
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
// [left,keyi-1][keyi+1,right]
if (left < keyi - 1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
StackDestory(&st);
}
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid][mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// 归并[begin, mid][mid+1, end]
printf("归并[%d,%d][%d,%d]\n", begin, mid, mid + 1, end);
}
注意事项:
我们分割的时候注意保证均分,不然递归的时候容易出现死循环。
递归展开图(部分):
归并到tmp,再拷贝回去。
一一归下来,拷贝回去。
两两归下来,拷贝回去。
四四归下来,拷贝回去。
整个归下来,拷贝回去。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid][mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
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++];
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
我们不用递归帮我们分割,直接用循环来分割。gap>=n,我们就不分了。
错误代码示范:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
// 间距为gap是一组,两两归并
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = i;
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++];
}
memcpy(a, tmp, n * sizeof(int));
//PrintArray(a, n);
gap *= 2;
}
free(tmp);
}
之所以说上面的代码是错误代码,是因为当数据的个数不是2的倍数时,程序就会崩溃。
我们按照之前的方法打印归并的区间,发现有些边界出现了越界。
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
// 间距为gap是一组,两两归并
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// end1 越界,修正
if (end1 >= n)
end1 = n - 1;
// begin2 越界,第二个区间不存在,保证begin2>end2即可
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
// begin2 ok, end2越界,修正end2即可
if (begin2 < n && end2 >= n)
end2 = n - 1;
//printf("归并[%d,%d][%d,%d] -- gap=%d\n", begin1, end1, begin2, end2, gap);
int index = i;
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++];
}
memcpy(a, tmp, n * sizeof(int));
//PrintArray(a, n);
gap *= 2;
}
free(tmp);
}