基本排序
选择排序
这个排序顾名思义,就是一趟找出最大和最小,然后最小插入头,最大插入尾,头++,尾--,一个中规中矩的排序罢了,和我们之后说的排序完全没有可比性。
void XuanZeSort(int* arr,int n)
{
int star = 0;
int end = n - 1;
while (star < end)
{
int min = star;
int max = star;
for (int i = star; i <= end; i++)
{
if (arr[i] < arr[min])
{
min = i;
}
if (arr[i] > arr[max])
{
max = i;
}
}
swap(&arr[star], &arr[min]);
if (star == max)
{
max = min;
}
swap(&arr[end], &arr[max]);
star++;
end--;
}
}
冒泡排序
这也算一个经典排序,不过它和选择排序半斤八两,性能都很一般。
//冒泡排序
void BubbleSort(int* arr,int n)
{
for (int i = 0; i < n; i++)
{
int tag = 0;
for (int j = 1; j < n - j; j++)
{
if (arr[j - 1] > arr[j])
{
swap(&arr[j - 1], &arr[j]);
tag++;
}
}
if (tag == 0)
{
break;
}
}
}
插入排序
这个就不那么一般了,可以说它还不错的,只是比不上那些更好的排序。
//插入排序
void insert(int* arr, int k)
{
for (int i = 0; i < k-1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
//每次用tmp和end比较 如果end小 那么就交换 然后end-- 继续对比 直到找到合适的位置
if (tmp < arr[end])
{
arr[end + 1] = arr[end];
--end;
}
//若tmp不是最小 那么就不需要继续对比下去 直接退出 进行赋值即可
else
{
break;
}
}
//由于while每次会--end 所以最后end处于-1的位置 需要+1才能对正,
另一种情况则是提前找到了break出来,那我们只要在它后面插入即可,
所以也+1
arr[end + 1] = tmp;
}
}
可以看出来,插入和上面的两位还是有点区别的,tmp巧妙地在成为参数的同时记录了原本的数据,使得我们不需要在创建一个新变量存储数据,只需要在tmp到达合适位置时把它放在end+1的位置即可。
大哥排序
希尔排序
//希尔排序
void Shell(int* arr,int k)
{
int gap = k;
while (gap > 1)
{
//gap具体是几其实没有定论 /3可以/4也可以 除的越大 预处理的范围越大,但也相对越无序,
//除的越小 范围越小 需要的次数也越多 但也越有序 为1时则是经典的插入排序
gap /= 3 + 1;
//-gap是因为tmp会有一个+gap的行为,所以如果设置成i<k则会发生越界的问题
for (int i = 0; i < k - gap; i++)
{
//将这个数据分为每组有gap个数据的小组 ,再对比这些间隔了gap个数据的大小 看是否需要进行交换
//大体逻辑其实和插入排序一样 不过是添加了一个根据gap对数据先进行预排序 以期望达到提高效率的过程
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = arr[end];
}
}
}
其实这就是插入排序的加强版,它巧妙的通过将数据分组,再把每组的数据进行粗略的预排序,然后再进行一次插入排序,所以这也可以算是加了优化的插入排序,不过它却实挺能打的。
堆排序
//堆排序
void HeapDown(int* pst, int size, int tmp)
{
//tmp是我们传入的父节点 那么*2+1则可得到子节点
int child = tmp * 2 + 1;
//只要孩子还小于我们的end 就继续循环
while (child < size)
{
//在进行比较交换前 先确定我们的 右孩子存在 且左孩子大于右孩子
//这样我们就++孩子 用大的那个
if (child + 1 < size && pst[child + 1] > pst[child])
{
child++;
}
//经过上面的if 现在孩子是大的那个
if (pst[child] > pst[tmp])
{
//交换父子位置
swap(&pst[tmp], &pst[child]);
//重新定义子位置和父位置 然后继续比较
//最后孩子会到它应该在的位置
tmp = child;
child = tmp * 2 + 1;
}
else
{
break;
}
}
}
void big(int* arr, int n)
{
for (int i = 1; i < n; i++)
{
//首先先建立一个大堆(或者小堆 看需求)
HeapDown(arr, i, 0);
}
//记录最后一个位置的下标
int end = n - 1;//例 k = 10 则end = 9
while (end > 0)//end决定我们走几次循环
{
//由于上面已经建立过堆了 所以首位一定是最大的,而末位一定是最小的 先交换它们
swap(&arr[0], &arr[end]);
//完成一趟 --end 然后看条件还满不满足 满足则继续排 最后有序
HeapDown(arr, end, 0);
--end;
}
}
这就利用了二叉树的大小堆结构来排序,但是由于大小堆不管孩子之间的关系,所以我们手动来创造条件,筛选出两个孩子中满足的进行交换,最后得到一个有序的数组。
快速排序
//三种快排
//三数取中
int FindMid(int* arr,int star,int end)
{
//头尾都好确定 唯独中间值 不好找 所以我们取 头尾中 这三个数中比较中位的数当中间值
int mid = (star + end) / 2;
if (arr[star] < arr[mid])
{
if (arr[mid] < arr[end])
{
return mid;
}
else if (arr[star] < arr[end])
{
return end;
}
else
{
return star;
}
}
else
{
if (arr[mid] > arr[star])
{
return mid;
}
else if (arr[star] < arr[end])
{
return star;
}
else
{
return end;
}
}
}
//霍尔快排
int sort(int* arr,int star,int end)
{
int open = star;
while (star < end)
{
//由于每次外层while进来都有可能改变end或star 所以外层的判断条件 里面还得判断一次
while (star < end && arr[end] >= arr[open])
{
--end;
}
while (star < end && arr[star] <= arr[open])
{
++star;
}
//尾找小 头找大 找到后交换它们的位置
swap(&arr[star], &arr[end]);
}
//出来后 再交换open 和 star 的位置即可返回 此时open才是star的初始位置 而star++多次后已经和end相遇
swap(&arr[open], &arr[star]);
return star;
}
//挖坑快排
int sort2(int* arr, int star, int end)
{
int open = arr[star];
//记录坑的下标
int K = star;
while (star < end)
{
//同霍尔方法一样 各找各的
while (star < end && arr[end] >= arr[open])
{
--end;
}
//此时end一定是小于坑的 所以 把它填坑里去 end成为新的坑
arr[K] = arr[end];
K = end;
while (star < end && arr[star] <= arr[open])
{
++star;
}
//此时star必定是大于新坑的 所以把它填进去 star变新坑
arr[K] = arr[star];
K = star;
}
//经过上面的多次变坑 最后star和end相遇停下 最后记录一下坑的位置然后返回 下次就从这继续 多次过后便有序了
arr[K] = open;
return K;
}
//前后指针快排
int sort3(int* arr, int star, int end)
{
//定义前后指针和参照物
int prev = star;
int next = star + 1;
int key = star;
while (next <= end)
{
//如果快指针小于参照物 那么交换它们 不过由于前后指针可能相遇 为了不进行不必要的交换 所以添加一个判断
//即慢指针走一步 如果不等于快指针 那才交换 否则的话 交不交换都一样 所以直接++快指针即可
if (arr[next] < arr[key] && ++prev != next)
{
swap(&arr[prev], &arr[next]);
}
++next;
}
//交换 返回
swap(&arr[star], &arr[prev]);
return prev;
}
void Quicksort(int* arr,int star,int end)
{
//三数取中优化版本
if (star>=end)
{
return;
}
//如果数据少于x个 那我们直接用别的排序快速给他排好返回即可 不必拆到底
if ((end - star + 1) < 10)
{
insert(arr + star, end - star + 1);
}
//经过三数取中筛选 每次都是最好的情况 效率大大提升
else
{
int mid = FindMid(arr, star, end);
swap(&arr[star], &arr[mid]);
int open = star;
int begin = star; int over = end;
while (begin < over)
{
while (begin < over && arr[over] >= arr[open])
{
--over;
}
while (begin < over && arr[begin] <= arr[open])
{
++begin;
}
swap(&arr[begin], &arr[over]);
}
swap(&arr[open], &arr[begin]);
open = begin;
Quicksort(arr, star, open - 1);
Quicksort(arr, open + 1, end);
}
//sort1-3都可以用二叉树前序来递归
/*int insert = sort(arr, star, end);
Quicksort(arr, star, insert - 1);
Quicksort(arr, insert + 1, end);*/
}
快排就有很多版本了,只能说都是大哥们写的优化,没有明显的强弱之分,看个人习惯选用即可,下面还有非递归版本的。
//非递归快速排序
#include "stack.h"//利用栈模拟实现递归的行为 故引入栈的文件
void NoDGQuickSort(int* arr,int star,int end)
{
//初始化栈
SL stack;
SLInit(&stack);
SLPush(&stack, end);
SLPush(&stack, star);
while (!SLempty(&stack))
{
//取出刚才存入的左右区间
int left = SLTop(&stack);
SLPOP(&stack);
int right = SLTop(&stack);
SLPOP(&stack);
//根据左右区间进行一次排序
int keyi = sort3(arr, left, right);
//再分出左右区间 下一次while继续排 模拟实现递归的步骤 最终有序
if (keyi + 1 < right)
{
SLPush(&stack, right);
SLPush(&stack, keyi + 1);
}
if (keyi - 1 > left)
{
SLPush(&stack, keyi-1);
SLPush(&stack, left);
}
}
SLDestroy(&stack);
}
我只能说在排序里叫快排的肯定有两把刷子,至少我在了解这些写法的时候总会忍不住喊个妙出来。
归并排序
//递归归并排序
void _MergeSort(int* arr,int star,int end,int* tmp)
{
//如果头尾相等 代表这个区间只有一个数 就不用排序了 直接返回上一层递归
if (star == end)
{
return;
}
//众所周知二叉树最后一层的数据占了整体的一半 所以递归到后面效率会变得很低 所以写了一个优化 让它不会递归到最后
//如此 在数据较少的情况下不进行二分 而是直接使用插入排序(当然其他也行 看个人) 进而提升效率
if (end - star + 1 < 10)
{
insert(arr + star, end - star + 1);
return;
}
//进来先划分区间
int mid = (star + end) / 2;
//以二叉树后序的方式进行递归
_MergeSort(arr, star, mid, tmp);
_MergeSort(arr, mid+1, end, tmp);
//若左右区间皆已返回 则代表上一层数据有序 此时再对比左右区间的数尾插
int star1 = star; int end1 = mid;
int star2 = mid + 1; int end2 = end;
int i = star;
while (star1 <= end1 && star2 <= end2)
{
if (arr[star1] < arr[star2])
{
tmp[i++] = arr[star1++];
}
else
{
tmp[i++] = arr[star2++];
}
}
//插到最后一定会剩一个最大的没插入 此时两个while只会进一个 又 因为有序 所以只要尾插就行
while (star1 <= end1)
{
tmp[i++] = arr[star1++];
}
while (star2 <= end2)
{
tmp[i++] = arr[star2++];
}
//最后将tmp有序的数组存到原本的arr上 +star是由于每次循环左右区间并不都是从头开始 所以需要加上偏移量
memcpy(arr + star, tmp + star, sizeof(int) * (end - star + 1));
}
void MergeSort(int* arr,int n)
{
//开一个保存数据的数组空间
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
assert(tmp);
return;
}
//用附属函数进行递归调用
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
//非递归归并排序
void MergeSort(int* arr, int n)
{
//同上 开空间
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
assert(tmp);
return;
}
//根据递归方法的思想 其实就是一直在二分 所以一进来 我们直接从最底层开始比较 也就是和递归方法反着来 故 gap设为1
int gap = 1;
while (gap < n)
{
//这个for循环其实就是在模拟每次二分的过程
for (int i = 0; i < n; i += 2 * gap)
{
//将数据带入区间
int star1 = i; int end1 = i + gap - 1;
int star2 = i + gap; int end2 = i + 2 * gap - 1;
//由于上面参数的设计 会出现越界的问题 为了不影响数据 所以做个保险 当star2都越界的情况下 end2必然也是越界的 所以不需要排这个区间的数据 直接退出即可
if (star2 >= n)
{
break;
}
//当end2越界 又因为上面if的判断 所以此区间只有一个end2越界 那我们只需要修改end2的边界即可
if (end2 >= n)
{
end2 = n - 1;
}
// 逻辑一样 故Ctrl+C Ctrl+V
int j = 1;
while (star1 <= end1 && star2 <= end2)
{
if (arr[star1] < arr[star2])
{
tmp[j++] = arr[star1++];
}
else
{
tmp[j++] = arr[star2++];
}
}
while (star1 <= end1)
{
tmp[j++] = arr[star1++];
}
while (star2 <= end2)
{
tmp[j++] = arr[star2++];
}
//需要和上面区分开来的是由于我们这种方式会越界 所以有时候有直接break的可能 所以每一次 都需要根据当时情况进行拷贝
//如果和上面一样最后进行一次cpy 那么数据会有多或者少的问题出现 所以在每一次循环末尾进行一次拷贝
memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
//最后 gap *= 2 模拟当层递归完成返回上一层的变化
gap *= 2;
}
//销毁空间
free(tmp);
tmp = NULL;
}
int main()
{
//Shell
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int k = sizeof(arr) / sizeof(arr[0]);
insert(arr, k);
return 0;
}