目录
1.排序的概念及其运用
1.1排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2排序运用
比如王者荣耀的排名
反正只要关于比大小的,就离不开排序。
1.3常见的排序算法
二.常见排序算法的实现
2.1插入排序
2.1.1直接插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
最后一层直接a[end + 1] = tmp;
//插入排序
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 (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
2.1.2希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
//希尔排序
void ShellSort(int* a, int n) {
int gap =n ;
while (gap>1)
{
//一般gap取数组大小的三分之一,这里的加1是确保最后一次gap=1
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
//一次排序
int end = i;
int tmp = a[gap + end];
while (end >= 0)
{
if (a[end] > tmp)
{
a[gap + end] = a[end];
end -= gap;
}
else
{
break;
}
}
a[gap + end] = tmp;
}
}
}
希尔排序的特性总结:
1.希尔排序就是对直接插入排序的优化;
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3.3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定:
《数据结构-用面相对象方法与C++描述》--- 殷人昆
因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:
4. 稳定性:不稳定
2.2选择排序
2.2.1 选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
但是这次我们换一种思想,每次选出最大和最小的数分别放在两边,让后再次选出最大最小的放在两边。
void Sweap(int* pa, int* pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//选择排序
void SelectSort(int* a, int n) {
int left=0;
//闭区间
int right=n-1;
while (left<right)
{
int mini = left;
int maxi = left;//这里只是下标,给什么都不影响
for (int i = left + 1; i <= right; i++) {
if (a[i]<a[mini])
{
mini = i; //找到最小的下标
}
if (a[i]>a[maxi])
{
maxi = i;//找到最大的下标
}
}
Sweap(&a[left], &a[mini]);//然后最小的放到最左边
if (left==maxi) //防止最大的是在第一个是时候,重叠
{
maxi = mini;
}
Sweap(&a[right], &a[maxi]);
left++;
right--;
}
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
2.2.2堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
这里的的细节参考:(32条消息) 数据结构-二叉树-详解_changgzhu的博客-CSDN博客
void Sweap(int* pa, int* pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//堆排序
void AdjustDwon(int* a, size_t size, size_t root) {
size_t parent = root;
size_t child = parent * 2 + 1;
while (child<size)
{
if (child+1<size&&a[child+1]<a[child])//这里我建的小堆
{
child++;
}
if (a[child]<a[parent])
{
Sweap(&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--)
{
AdjustDwon(a, n, i);
}
int end = n - 1;
while (end>0)
{
Sweap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
end--;
}
}
堆排序的总结:
1. 堆排序使用堆来选数,效率就高了很多
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
2.3交换排序
2.3.1冒泡排序
基本就是思想就是:数据里每个数,跟全部比较,得出的结果。(因为这是很简单的排序,直接上代码)
void Sweap(int* pa, int* pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//冒泡排序
void BubbleSort(int* a, int n) {
int i = 0;
int j = 0;
for ( i = 0; i < n; i++)
{
int exchange = 0;
for ( j = 1; j < n-i; j++)
{
if (a[j - 1] < a[j]) {
exchange = 1;
Sweap(&a[j - 1], &a[j]);
}
}
if (exchange==0)
{
break;
}
}
}
冒泡排序的总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
2.3.2快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。
快速排序可以分为2大块
递归 和非递归
一.递归排序
1.hoare版本
这个方法也是Hoare这个人提出来的,大致思路:
1.在一组数据中选出一个数作为基数key(一般选第一个或者最后一个)
2.定义2个指针,分别指向第一个L,和最后一个R
3.R找比key小的,L找比key大的,找到后交换,再继续,直到相遇停止。
4.然后把相遇的位置与key交换位置。
注意:假如选第一个,R先走,这样可以保证R先停下来,L走去遇到R,相遇的位置比key小
同理假如选最后一个,L先走。
然后可以把数据分为和
然后再将这个两个数据,像第一次一样操作。
这也就可以看作树的方式用递归的方式解决问题
int PartSort1(int* a, int left, int right) {
int key = left;
while (left<right)
{
while (left < right && a[right] >= a[key]) {
right--;
}
while (left<right&&a[left]<=a[key])
{
left++;
}
Sweap(&a[left], &a[right]);
}
Sweap(&a[left], &a[key]);
return left;
}
void QuickSort(int* a, int begin, int end) {
//这里是闭区间
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin>=end)
{
return;
}
int key = PartSort1(a, begin, end);
QuickSort(a, begin, key - 1);
QuickSort(a, key+1, end);
}
2.挖坑法
大致过程:
1.选出一个基数(一般也是左边或者右边),然后用一个临时变量key存储。
2.定义2个指针,分别指向第一个L,和最后一个R。
3..R找比key小的,L找比key大的,找到后与空的那个交换。
4.当L跟R相遇时,把key放在他们相遇的位置上
然后把数据分为
然后再次重复过程
又类似与树的结构。
与hoare版本没有本质区别,但是好理解
不需要理解为什么左边做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;
}
void QuickSort2(int* a, int begin, int end) {
//这里是闭区间
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
{
return;
}
int key = PartSort2(a, begin, end);
QuickSort2(a, begin, key - 1);
QuickSort2(a, key + 1, end);
}
3.前后指针法
大致思路:
1.同样选一个基数key(一般最左或者最右)
2.定义两个指针prve指向第一个,cur指向prve的前一个。
3.cur找比key小的时候,++prve,交换prve与cur的位置的值。
4.cur还没有找到比key大的值时,prve紧跟cur一前一后。
5.prve遇到比key大的值时候会停下来,然后等待cur找到小的值,进行交换。
6.当cur走完时候,prve会跟key交换值
接下数据又分成树的形状。
//前后指针法
int PartSort3(int* a, int left, int right) {
int key = left;
int prve = left;
int cur = left + 1;
while (cur<=right)
{
//这里的prve只要在cur遇到比key小的值以后才可以++
// != 这里是防止自己跟自己交换
if (a[cur] < a[key] && a[++prve] != a[cur]) {
Sweap(&a[cur], &a[prve]);
}
cur++;
}
Sweap(&a[prve], &a[key]);
return prve;
}
void QuickSort3(int* a, int begin, int end) {
//这里是闭区间
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end)
{
return;
}
int key = PartSort3(a, begin, end);
QuickSort3(a, begin, key - 1);
QuickSort3(a, key + 1, end);
}
4.递归的优化
优化1:
上面三种排序
最好的情况:是选的key都是中位数,时间复杂度是O(N*LogN)
然后树都成这样:
最坏的情况:每次选的都是最大的或者最小的,或者有序接近有序,时间复杂度却是O(N^2)
然后树成这样:
所以为了让key选不到最大的和最小的,我们来一个三数取中。
//三数取中
int GetMidIndex(int* a, int left, int right) {
int mid = left + (right - left) / 2;
//mid left right
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;
}
}
}
在上面三种方法开头假如这段代码,就可以保证不会选出最大或者最小的
int PartSort3(int* a, int left, int right) {
int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]);
int key = left;
int prve = left;
int cur = left + 1;
while (cur<=right)
{
//这里的prve只要在cur遇到比key小的值以后才可以++
// != 这里是防止自己跟自己交换
if (a[cur] < a[key] && a[++prve] != a[cur]) {
Sweap(&a[cur], &a[prve]);
}
cur++;
}
Sweap(&a[prve], &a[key]);
return prve;
}
优化2:小区间优化
递归版快速排序展开图可以看作一个二叉树
当区间很小时,可以不再用递归划分的思路让他有序,而是用插入排序对小区间进行排序,减少递归。如:
//挖坑法
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;
}
void QuickSort2(int* a, int begin, int end) {
//这里是闭区间
// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
if (begin >= end){
return;
}
// 小区间直接插入排序控制有序
if (end - begin + 1 <= 3)//这是一个闭区间,所以要加1
{
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);
}
}
二 .非递归
这里我们需要一个栈的数据结构,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;//栈顶的位置
int capacity;
}ST;
//初始化
void StackInit(ST* ps);
//销毁栈
void StackDestroy(ST* ps);
//尾插
void StackPushBack(ST* ps, STDataType x);
//尾删
void StackPopBack(ST* ps);
//判空
bool StackEmpty(ST* ps);
//打印
STDataType StackTop(ST* ps);
//数量
int StackSize(ST* ps);
//初始化
void StackInit(ST* ps) {
assert(ps);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
//销毁栈
void StackDestroy(ST* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
//尾插
void StackPushBack(ST* ps, STDataType x) {
assert(ps);
if (ps->top==ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
ps->a = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (ps->a==NULL)
{
printf("realloc fail\n");
exit(-1);
}
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
//尾删
void StackPopBack(ST* ps) {
assert(ps);
assert(ps->top > 0);
ps->top--;
}
//判空
bool StackEmpty(ST* ps) {
assert(ps);
return ps->top == 0;
}
STDataType StackTop(ST* ps) {
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
//数量
int StackSize(ST* ps) {
assert(ps);
return ps->top;
}
其实我们也是模仿递归的方式来排序,大致思路:
1.创建一个栈,然后首先把头尾区间入栈,出栈,然后经历第一次排序得到key
2.然后再类似树的结构,分为左右区间,在重复第一步。
//非递归
void QuickSort4(int* a, int begin, int end) {
ST st;
StackInit(&st);
StackPushBack(&st, begin);
StackPushBack(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);
StackPopBack(&st);
int left = StackTop(&st);
StackPopBack(&st);
int key = PartSort3(a, left, right);//三种随便选一种
//左右可以互换
if (left<key-1)
{
StackPushBack(&st, left);
StackPushBack(&st,key-1);
}
if (key+1<right)
{
StackPushBack(&st, key + 1);
StackPushBack(&st,right);
}
}
StackDestroy(&st);
}
2.4归并排序
也有递归和非递归
一.递归
思路很简单:就是一组数据先分解,合并。在合并过程种排序。
这里合并的时候我们单独创建一个空间tmp,在里面进行排序然后返回原数组,等到全部排序好,再统一memcpy到原数组里。
代码如下:
void MergeSort1(int* a, int begin, int end, int* tmp) {
if (begin>=end)
{
return;
}
int mid = (begin + end) / 2;
MergeSort1(a, begin, mid, tmp);
MergeSort1(a, mid + 1, end, tmp);
//归并[begin, mid][mid+1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1<=end1&&begin2<=end2)
//进tmp里面
{
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));
}
//归并
void MergeSort(int* a, int n) {
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
MergeSort1(a, 0, n - 1, tmp);
free(tmp);
}
二。非递归
思路挺简单的:
跟递归的方法差不多,先分再合。
//非递归
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越界时,第二个区间不存在
if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
//end2越界时,修正
if (begin2 < n && end2 >= n)
{
end2 = n - 1;
}
printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
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, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
2.5计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列种
但是也有这样的数据
我们如果绝对映射开0到10000 的空间,我们会浪费0到5000个空间,这时候我们要相对映射。
//计数排序
void CountSort(int* a, int n) {
int min=0, max=0;
for (int i = 0; i < n; i++)
{
//选出最大最小值
if (a[i] < min) {
min = a[i];
}
if (a[i]>max)
{
max = a[i];
}
}
//加1,用因为这是下标
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
assert(count);
memset(count, 0, sizeof(int) * range);
//计数
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;
}
}
}
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(range + N)
3. 空间复杂度:O(range)