插入排序:
原理
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移 ( 即每次从后面向前插入一个新的元素并和前面的元素进行比较,找到合适的位置插入)
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定 (意思是如果原本有两个5, 5 5 ,原来排在前面的再排序后仍旧排在前面)
void InsertSort(int* a, int n)
{
//控制循环次数 ,时间复杂度O(n^2)
for (int i = 0; i < n - 1; ++i)
{
//每次把end 以后的数字插入前面的数中
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;
}
}
希尔排序法:
原理
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成 gap 个组,所有距离为 gap 的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工
作。当到达 gap == 1 时,所有记录在统一组内排好序。
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
-
当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^ (1.3—2) )
- 稳定性:不稳定
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 5 + 1; //保证最后一次一定为1
//控制循环次数 ,时间复杂度O(n^2)
for (int i = 0; i < n - gap; ++i)
{
//每次把end 以后的数字插入前面的数中
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;
}
}
}
堆排序法:
原理
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度: O(N*logN)
- 空间复杂度: O(1)
- 稳定性:不稳定
必备知识点
如果要从一亿个随机数中找出10个最大的数怎么办?
这个时候用堆排是最快的,随便取10个数建一个小根堆,让堆顶元素去比较,比交换然后向下调整,循环往复,最终剩余的10个数就是最大的。
//向下调整建立大堆
void AdjustDown(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
++child;
if (a[parent] < a[child])
{
swap(&a[parent],&a[child]);
}
else
break;
}
}
void HeapSort(int* a, int n) //升序建立大堆,建堆
{
for (int i = (n - 2) / 2; i >= 0; --i) //向下调整建立大堆
AdjustDown(a, n, i);
int end = n - 1;
while (end > 0) //剩下最后一个数即是最小值
{
swap(&a[0], &a[end]); 把树头 和 树尾 交换,然后重新调整n-1 个数
AdjustDown(a, end, 0);
--end;
}
}
冒泡排序:
原理
交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N) ~ O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
void sort(int *arr, int len) //正序
{
for (int i = 0; i < len - 1; ++i) //控制循环次数
{
for (int j = 0; j < len - i - 1; ++j) //每一轮循环后最右边的已经是最大的,故下次只需从0遍历 len - i个
{
if (arr[j] >arr[j + 1]) //把大值往后面换
{
int tmp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tmp;
}
}
}
}
void sort1(int *arr, int len) //逆序
{
for (int i = 0; i < len - 1; ++i) // 控制循环次数
{
for (int j = len - 1; j >i; --j)//每次循环后左边都为最小,所以只需遍历到 i 之前的数即可
{
if (arr[j] > arr[j - 1]) //把大值往前面换
{
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
}
}
}
}
快速排序法
原理
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
将区间按照基准值划分为左右两半部分的常见方式有:
- hoare版本
- 挖坑法
- 前后指针版本
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN) ~ O(N^2)
- 空间复杂度:O(logN)
- 稳定性:不稳定
1.挖坑法
void quick_sort(int* nums, int left, int right)
{
if (left < right) //递归终止条件
{
int x = nums[left], l = left, r = right;
while (l < r) //循环条件
{
while(l < r && x <= nums[r]) // 找小 给左
--r;
if (l < r) //赋值条件
nums[l] = nums[r];
while (l < r && x >= nums[l]) //找大 给右
++l;
if (l < r) //赋值条件
nums[r] = nums[l];
}
nums[l] = x; //将x补给最后一个被给与其他位置的数
quick_sort(nums, left, l - 1); //递归
quick_sort(nums, l + 1, right);
}
}
2.前后指针法
void QuickSort3(int* a, int left, int right)
{
if (left < right)
{
int cur = left, prev = left - 1, key = a[right];
while (cur < right)
{
if (a[cur] < key ) //小于key 则交换 cur 和 prev的值
{
++prev;
swap(&a[cur], &a[prev]);
}
++cur;
}
++prev; //交换
swap(&a[right],&a[prev]);
QuickSort3(a, left, prev - 1);
QuickSort3(a, prev + 1, right);
}
}
3.hoare法
void QuickSort2(int* a, int left, int right)
{
if (left < right)
{
int key = a[right], l = left, r = right;
while (l < r)
{
while(l < r && a[l] <= key)
++l;
while(l < r && a[r] >= key)
--r;
swap(&a[l], &a[r]);
}
swap(&a[l], &a[right]); //必须交换,不然最后的值一直没有改变
QuickSort2(a, left, l - 1);
QuickSort2(a, l + 1, right);
}
}
4.快排挖坑非递归法(需要借助栈来模拟递归)
void QuickSortNonR1(int* a, int left, int right)
{
Stack st;
StackInit(&st); //初始化栈
StackPush(&st, left); //入栈,先入哪个都行,但记得前后保持一致
StackPush(&st, right);
while (!StackEmpty(&st)) //当栈不为空
{
int _r = StackTop(&st); //获取栈顶元素
StackPop(&st); //出栈
int _l = StackTop(&st);
StackPop(&st);
int l = _l, r = _r, key = a[_r];
while (l < r)
{
while (l < r && a[l] <= key)
++l;
if (l < r)
a[r] = a[l];
while (l < r && a[r] >= key)
--r;
if (l < r)
a[l] = a[r];
}
a[l] = key;
if ( _l < l - 1)
{
StackPush(&st, _l); //入 _l, l - 1
StackPush(&st, l - 1);
}
if (l + 1 < _r)
{
StackPush(&st, l + 1); //入 l + 1, _r
StackPush(&st, _r);
}
}
StackDestroy(&st); //销毁栈
}
归并排序法:
原理
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
void Merge(int* a, int left, int right, int* tmp)
{
if (left >= right) //当只剩一个元素时直接返回
return;
int mid = (left + right) >> 1;
//分组,每次递归分为[left,mid] [mid+1,right]
Merge(a, left, mid, tmp);
Merge(a, mid + 1, right, tmp);
int l1 = left, r1 = mid, l2 = mid + 1, r2 = right;
int i = left;
while (l1 <= r1 && l2 <= r2)
{
if (a[l1] < a[l2])
tmp[i++] = a[l1++];
else
tmp[i++] = a[l2++];
}
if (l1 <= r1) //此时l1未排完,全部放入tmp
{
while(l1 <= r1)
tmp[i++] = a[l1++];
}
else //l2未排完,全部放入tmp
{
while (l2 <= r2)
tmp[i++] = a[l2++];
}
memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1)); //每次把归并好的那一组拷贝给a
}
//归并排序
void MergeSort(int* a,int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
Merge(a, 0, n - 1, tmp);
free(tmp); //释放tmp
}
选择排序法:
原理
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
//每次从未排序的数据中选取最小值,并把最小值放在未排序数据的起始位置,直到未排序的数据为0,则结束排序。
void selectSort(int* arr, int n)
{ //i: 未排序数据的起始位置
for(int i = 0; i < n; ++i)
{
int minIdx = i;
//从所有未排序的数据中找最小值的索引
for(int j = i + 1; j < n; ++j)
{
if(arr[j] < arr[minIdx])
minIdx = j;
}
swap(arr[minIdx], arr[i]);
}
}
计数排序:
原理
计数排序是一种非基于比较的排序算法,其空间复杂度和时间复杂度均为 O(n+k),其中 k 是整数的范围。基于比较的排序算法时间复杂度最小是 O(nlogn)的。计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序的特性总结:
- 时间复杂度 O(N+K)
- 空间复杂度 O(N+K)
- 如果最大、最小值相差太大,就不建议使用了,会浪费太大空间
- 稳定性:稳定
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
//找max 和 min
for (int i = 0; i < n; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
//求range
int range = max - min + 1;
int* countArr = (int*)malloc(sizeof(int) * range);
memset(countArr, 0, sizeof(int) * range); //初始化空间
//1、统计次数
for (int i = 0; i < n; ++i)
{
countArr[(a[i] - min)]++;
}
//2、根据次数进行排序
int j = 0;
for (int i = 0; i < range; ++i)
{
while (countArr[i]--)
a[j++] = i + min;
}
//释放空间,指针置NULL
free(countArr);
countArr = NULL;
}