一.插入排序
插入排序的主要思想类似于摸扑克牌,在已经有序的一个区间内插入新的值,找到合适的位置插入,然后再将原来位置的值后移。
//插入排序
//时间复杂度O(N^2)
//最坏情况:逆序,987654321,需要移动的次数为1,2,3,4,....n-1,等差数列
//最好情况,顺序,O(N)
void InsertSort(int* a, int n)
{
// [0,end]有序,把end+1位置的值插进去,让[0,end+1]有序
for (int i = 0; i < n - 1; i++) //n-1,防止越界
{
int end = i; //从0开始插入,类似摸扑克牌
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;//无论是循环结束还是中途结束,都是break,然后把数放在end的后一个
}
}
二.希尔排序
希尔排序可以看作插入排序的一种升级,当要排序的数非常多时,我们可以设置一个gap,例如gap为5,则5个数据被分为一组,然后间隔5个空格对数据进行排序,然后不断缩小gap进行排序,当gap最后为1时,其实就是插入排序。
//希尔排序
//1.先进行预排序,让数组接近有序 2.直接插入排序
//多组间隔为gap的预排序,gap由大变小,gap越大,大的数越快的到后面,小的数越快到前面
//gap越大,预排序完越不接近有序,gap越小,越接近有序,当gap==1时,就是插入排序
//时间复杂度:O(logN*N)
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1) //2^x = N ,时间复杂度为logN(2为底)
{
gap = gap / 2; //保证最后一次为1,即插入排序,gap>1都是预排序
//gap = gap / 3 + 1;
//这里每次i只会增加一个,也就是end每次向后增加一个,这样可以把间隔为gap的多组数据同时排
//gap很大时,下面预排序时间复杂度O(N), gap很小时,预排序已经完成,很接近有序,还是O(N)
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
三.直接选择排序
选择排序的思路很简单,从数组两头开始找到一个最大的和一个最小的数,分别放在两端,然后更新区间继续寻找最大的数和最小的数,注意一下代码中的细节
//直接选择排序 O(N^2) N+N-1+N-2+......
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end) //更新区间
{
int mini = begin, maxi = begin;
for (int i = begin; i <= end; i++) //在区间内找到一个最大的和最小的
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
if (begin == maxi) //防止begin==maxi时,最大值被换走,更新一下下标
{
maxi = mini;
}
Swap(&a[maxi], &a[end]);
++begin;
--end;
}
}
四.堆排序
1.堆的物理结构和逻辑结构
堆的物理结构是一个数组,逻辑结构是一颗完全二叉树
其中大堆表示父节点的值比孩子节点的值大,小堆是父节点的值比孩子节点的值小,堆排序就是借助堆帮助选择,因为堆顶的元素是最大或者最小的。
2.建堆和向下调整算法
1.首先把数组想象成一个完全二叉树,然后进行建堆
2.向下调整算法。算法前提:左右子树都是大堆或者小堆
过程:从根节点开始,选出左右孩子较小或较大的那个和父亲比较,满足条件则交换,然后继续往下,调到叶子节点终止。正是因为左右子树均是大堆或者小堆,才能完成这个交换
void AdjustDown(int* a,int n,int root) //向下调整算法,必须满足左右子树是大堆or小堆,这里是大堆
{
int parent = root;
int child = parent * 2 + 1; //默认是左孩子
while (child < n)
{
//1.选出左右孩子中大的那个
if (a[child + 1] > a[child] && child + 1 < n) //防止越界
{
child += 1;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
但是对于一个一般的数组,左右子树并不满足大堆或者小堆,这里不能直接使用向下调整算法。 要自下而上,从最后一颗子树开始调整,因为叶子节点只有一个,不需要调,从最后一个非叶子节点开始调。
在这里我们先对8这个数进行调整,再对7,然后是2,接着是5,这样自下而上调整,当我们要调整顶上时,下面已经是调整好的大堆或者小堆,还是利用节点之间的父子关系找到8。
//建堆 时间复杂度O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i) //倒数的第一个非叶子节点开始调整 ,child=(parent-1)/2
{
AdjustDown(a, n, i);
}
接着我们需要考虑排升序应该是建大堆还是小堆,在这里我们应该建大堆,每一次将最大的换到最后,然后不把他看作堆里的值,此时左右仍满足大堆,前n-1个数向下调整找出次大的值。
这里图二是建小堆,如果这样做的话,虽然可以找到最小的数,但是找次小的数时,需要拿第二个数做根,此时的父子关系乱了,左右也不满足小堆,又需要重新建堆,效率太低,不如直接遍历
void HeapSort(int* a,int n) //整体时间复杂度:O(N*logN)
{
//建堆 时间复杂度O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i) //倒数的第一个非叶子节点开始调整 ,child=(parent-1)/2
{
AdjustDown(a, n, i);
}
//排升序,建大堆,如果这里建小堆,可以找到最小的数,
//但是找次小的数的时候需要拿第二个数去做根,剩下的树的父子关系全乱了,只能重新建堆,效率太低,不如直接遍历
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]); //最大的换到最后,不把它看做堆里面的,此时左右子树都满足大堆,向下调整找出次大的
AdjustDown(a, end, 0);
end--;
}
}
到此堆排序完成
五.快速排序
快速排序的基本思想:
![](https://img-blog.csdnimg.cn/direct/c3ccdd1abfac418892c39aa2d97030ed.jpeg)
1.挖坑法
//挖坑法
int PartSort1(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int begin = left, end = right;
int pivot = begin;
int key = a[begin];
while (begin < end)
{
//右边找小,放到左边
while (begin < end && a[end] >= key)
{
--end;
}
//小的放到左边的坑里,同时自己形成坑位
a[pivot] = a[end];
pivot = end;
//左边找大,放在右边
while (begin < end && a[begin] <= key)
{
++begin;
}
//大的放到右边的坑里,自己形成新的坑
a[pivot] = a[begin];
pivot = begin;
}
pivot = begin;
a[pivot] = key;
return pivot;
}
2.左右指针法,与挖坑法类似
//左右指针法
int PartSort2(int* a,int left,int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int begin = left, end = right;
int keyi = begin;
while (begin < end)
{
//找小
while (begin < end && a[end] >= a[keyi])
{
--end;
}
//找大
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
return begin;
}
3.前后指针法
cur找小,每次遇到比key小的值就停下来,++prev,然后交换
当cur和prev距离变大时,说明中间有很多比key大的数,如果这个时候再找到一个比key小的,++prev就指向了大的那个数,然后将大的换到右边小的换到左边。最后再把prev和key交换,就满足快速排序的基本思想
//前后指针法
int PartSort3(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
++prev;
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
4.快速排序的改进
1.三数取中。快速排序对基准的选择很重要,如果序列是有序的,快排的效率会很低,用三数取中改进这个问题
//三数取中 解决快排在有序情况下效率低的问题
int GetMidIndex(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 right;
}
else
{
return left;
}
}
else //a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
2.小区间优化
//快速排序
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
//int keyIndex = PartSort1(a, left, right);
// int keyIndex = PartSort2(a, left, right);
int keyIndex = PartSort3(a, left, right);
//QuickSort(a, left, keyIndex - 1);
//QuickSort(a, keyIndex + 1, right);
//小区间优化
if (keyIndex - 1 - left > 10)
{
QuickSort(a, left, keyIndex - 1);
}
else
{
InsertSort(a + left, keyIndex - 1 - left + 1);
}
if (right - (keyIndex + 1) > 10)
{
QuickSort(a, keyIndex + 1, right);
}
else
{
InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);
}
}
5.快速排序的非递归
前面我们实现的快速排序都是使用递归,但是递归的缺陷是可能会造成栈溢出。这里我们利用数据结构中的栈来完成快速排序的非递归。利用栈存放需要排序的数组。当完成第一趟快排后,将数组划分成两段存到栈里,因为栈是后进先出,所以如果想要先处理左边的,就要先把右边的数组压栈。然后再对左区间出栈,进行第二趟快速排序,等到栈空时,排序就完成了。
//快速排序非递归 利用数据结构栈模拟过程
void QuickSortNonR(int* a, int n)
{
ST st;
StackInit(&st);
StackPush(&st, n - 1);
StackPush(&st, 0);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyIndex = PartSort1(a, left, right); //单趟排序
if (keyIndex + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyIndex + 1);
}
if (left < keyIndex - 1)
{
StackPush(&st,keyIndex - 1);
StackPush(&st,left);
}
}
StackDestory(&st);
}
六.归并排序
归并排序是一种分治思想,当我想要整个序列有序,可以先让左边和右边有序,但是左区间和右区间怎么有序呢,这就可以对左右区间继续划分,当划分到不可分割的子问题时,就可以认为是有序的了,然后对数组进行合并。在合并的过程中,我们需要利用好每个数组都是有序的这个性质,每次在两个数组中取最小的放入新数组,这样整个数组就有序了。
//归并排序
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) >> 1;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++]; //归并,依次对比取小的放进新的临时数组
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
//拷贝回去
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);
}
七.计数排序
计数排序的思想很特别,是一种非比较的排序,他利用了一个有序的数组进行排序。
但是这里的数字和下标的对应不是绝对的,假设数据是100 101 103 106 109,我们这里开辟空间不应该从0开始,不然会浪费空间,我们应该使用相对的映射位置,即num-min,减去最小的那个值。这样的话100就对应在下标0,以此类推。
//计数排序 ,统计出现的次数,在对应的数组上做映射
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* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range); //初始化次数为0
for (int i = 0; i < n; i++)
{
count[a[i]-min]++; //统计位置上出现的次数
}
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j] = i + min;
j++;
}
}
free(count);
}