Impossible=I’m possible
只要你努力了,就没什么是不可能的!
目录
1.插入排序
1.1.直接插入排序
基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实现方法:
代码如下:
void InsertSort(int* a, int n) //插入排序
{
for(int i=0;i<n-1;i++)
{
int end=i;//比较的值
int tmp=a[end+1];//插入的值
while(end>=0)
{
if(tmp<a[end])//tmp小于当前值,把end所指向的位置空出来,然后把tmp插进去
{
a[end+1]=a[end];
end--;
}
else
{
break;//不小于 跳出
}
a[end+1]=tmp;//插入
}
}
}
特性总结:
1.元素集合越接近有序,直接插入排序算法的时间效率越高
2.时间复杂度:
最好的情况:(已经有序)O(N)
最坏的情况:O(N^2)
3.空间复杂度: O(1 ),它是一种稳定的排序算法
4.稳定性:稳定
1.2.希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1.插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
2.但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
实现方式:
代码如下:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1) //判断是否还需排序
{
//gap = gap / 2;
gap = gap / 3 + 1;
// [0,end] 插入 end+gap [0, end+gap]有序 -- 间隔为gap的数据
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;
}
}
}
希尔排序的特性总结:
1.希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。
4.时间复杂度:O(nlog n)(平均时间复杂度)
5.空间复杂度:O(1)
6.稳定性:不稳定
2.选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
2.1.直接选择排序
实现方式:
代码如下:
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
// 选出最小的放begin位置
// 选出最大的放end位置
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
// 修正一下maxi
if (maxi == begin)
maxi = mini;
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.2堆排序
基本思想:堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
实现方式:
代码如下:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(int* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
// 找出小的那个孩子
if (minChild + 1 < n && a[minChild + 1] > a[minChild])
{
minChild++;
}
if (a[minChild] > a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
// O(N*logN)
void HeapSort(int* a, int n)
{
// 大思路:选择排序,依次选数,从后往前排
// 升序 -- 大堆
// 降序 -- 小堆
// 建堆 -- 向下调整建堆 - O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
// 选数 N*logN
int i = 1;
while (i < n)
{
Swap(&a[0], &a[n - i]);
AdjustDown(a, n - i, 0);
++i;
}
}
特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
3.交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
3.1.冒泡排序
基本思想:比较相邻的元素,如果第一个比第二个大,就交换他们两个。对每一对相邻的元素做同样的工作。
实现方法:
代码如下:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//冒泡
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int exchange = 0;//来判断序列是否有序
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
int main()
{
int a[] = { 1,4,6,8,10,2,5 };
int sz = sizeof(a) / sizeof(a[0]);
BubbleSort(a, sz);
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
return 0;
}
冒泡排序的特性总结:
1.冒泡排序是一种非常容易理解的排序
2.时间复杂度:O(N^2)
3.空间复杂度:O(1)
4.稳定性:稳定
3.2.快速排序
1.思想:
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
以下是排序要用到的递归:代码如下:
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
// 按照基准值对a数组的 [left, right]区间中的元素进行划分
int keyi = PartSort3(a, begin, end);
// 划分成功后以keyi为边界形成了左右两部分 [begin, keyi-1] 和 [keyi+1, end]
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉
树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。
1.递归版本
(1).honre版本
具体步骤:
1.选一个key.(一般是第一个或者最后一个)
2.定义一个begin,一个end,begin从左到右走,end从右向左走。
3.当end遇到比key小的值停下,begin开始走,直到begin遇到一个比key大的值,将此时的begin和right内容进行交换。end继续走,直到与begin相遇,然后把相遇点的内容与key交换即可。
4.这样得到的就是左边都是小于key的值,右边都是大于key的值。
5.最后,再分别对左右进行递归,做以上步骤,就可以得到有序的序列。
注意:相遇位置的值如何保证比key的值要小,R先走
(1).L撞到R相遇:R找小,找到key小的值然后停下来,L找比key大的值,相遇的时候,相遇位置的值肯定比key的值小
(2).R撞到L相遇:R找小,找到key小的值然后停下来,L找比key大的值,然后交换,然后R继续找小,撞到L,相遇点的值是交换后的值,所以key的值一定小。
代码如下:
int PartSort(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left<right && a[right]>a[keyi])
{
right--;
}
//L找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
//交换
if (left < right)
Swap(&a[left], &a[right]);
//相遇点
int meeti = left;
Swap(&a[meeti],&a[keyi]);//与相遇位置交换
return meeti;
}
}
三数取中法
优化key逻辑:
1.随机选一个位置做key
2.针对有序,选正中间值做key
3.三数取中。第一个 中间位置 最后一个 选出中间值
引入三数取中的方法,将有序或接近有序的序列的首元素值调整为该序列的中间值,从而就形成了二叉树形式的递归了,从而也就解决了递归深度为N造成栈溢出的问题了!
代码如下:
//三数取中
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[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else //a[left]>=a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
三数取中的用法:
// [left, right] -- O(N)
// hoare
int PartSort1(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);//把首元素值调整为该序列的中间值
int keyi = left;
while (left < right)
{
//R找小
while (left<right && a[right]>=a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <=a[keyi])
{
left++;
}
if (left < right)
Swap(&a[left], &a[right]);
}
int meeti = left;//相遇位置比keyi小
Swap(&a[meeti], &a[keyi]);
return meeti;
}
(2).挖坑法
思想:先把最左边的位置设为坑并且设置为key,从右边开始向左走,找到比key小的值,然后把该位置的数放到坑中,更新坑位与key的值,然后从左边开始走,找比key大的值,再次更新key与坑的值。相遇的时候,将初始保存的值放到相遇的坑位。
代码如下:
//挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);//把首元素值调整为该序列的中间值
int key = a[left];
int hole = left;//坑的位置
while(left<right)
{
//右边找小,填到左坑
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];//值交换
hole = right; //位置交换
//左边找大,填到右边坑
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
return hole;
}
(3).前后指针法
思路:初始化,prev指针指向序列开头,cur指针指向prev的后一个位置。cur遇到比key小的值时,就停下来,然后++prev,最后交换prev与cur的位置的值。
代码如下:
//前后指针法
//cur遇到比key小的值,就停下来
//++prev,交换prev和cur的位置的值
int PartSort3(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);//把首元素值调整为该序列的中间值
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
//找小
if (a[cur] < a[keyi] && ++prev != cur) //注意:++prev!=cur
Swap(&a[cur], &a[prev]);
++cur;//不管怎样,一直走
}
Swap(&a[keyi], &a[prev]);//最后,交换(此时prev>keyi)
return prev;
}
2.非递归实现
非递归实现快排需要借助数据结构中的栈。
代码如下:
void QuickSortNonR(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);
/*if (left >= right)
{
continue;
}*/
int keyi = PartSort3(a, left, right);
// [left, keyi-1] keyi [keyi+1,right]
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
if (left < keyi - 1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
}
StackDestroy(&st);
}
先把整个大区间压入栈中,在进入while循环,注意先进后出的顺序,这里先接受right,后接受left,在进行排序,再用返回的key把区间再次分开,进行重复的操作即可。
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(NlogN),即每层的操作次数树的深度。
- 空间复杂度:O(logN)
- 稳定性:不稳定
4.归并排序
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。通过递归实现对小数组有序,再返回回来。
实现方法:
代码如下:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (end + begin) / 2;
// [begin, mid] [mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// 归并 取小的尾插
// [begin, mid] [mid+1, end]
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");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
5.非比较排序
5.1计数排序
思想:1.统计相同元素出现次数
2.根据统计的结果将序列回收到原来的序列中
具体如下:(1)统计相同元素出现的次数
给定一个数组a,开辟一个计数数组count,a[i]是几,就对count数组下标是几++,如下图所示:
当然如果数字比较大的时候,就要用到相对映射的关系,例如有101、105…这样的数值,count数组的空间大小就用最大值-最小值+1来表示(range=max-min+1),得到count数组下标是j=a[i]-min。如下图所示:
(2)根据count数组的结果,将数据拷贝到a数组,count[j]中数据是几,说明该数出现的次数。
代码如下:
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] > min)
{
min = a[i];
}
}
//范围
int range = max - min + 1;
//统计次数 开辟空间
int* countA = (int*)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;
++j;
}
}
free(countA);
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
6.排序算法复杂度及稳定性分析
稳定:直接插入排序、冒泡排序、归并排序
不稳定:希尔排序、选择排序、堆排序、快速排序、计数排序