冒泡排序
算法思路:
- 从左到有依次遍历整个待排序数据,相邻的两个元素之间比较,将较大的数据交换到后面——>最大元素被置换到最后一个位置。
- 循环处理前面的数据,每次都会少处理一个数据
void BubbleSort(int *arr,int len)
{
//趟数控制
//每趟控制都比上一次少处理一个数据
//第一次需要全部遍历
//最后一次只剩下一个元素,无需比较
//因此趟数为:len - 1
for(int k = 0; k < len -1;k++)
{
//一次比较:将最大的元素置到最后
//i+1不能越界
//每次要处理的数据个数为len - k;
for(int i = 0; i +1 < len - k ;i++)
if(arr[i] > arr[i+1])
{
SwapData(&arr[i],&arr[i+1]);
}
}
}
效率分析:
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
选择排序
算法思路:
先遍历整个数据,标记处最小数据的位置,然后将最小的数据与当前第一个位置交换。
void SelectSort(int *arr,int len)
{
//k的作用:
//1.控制趟数为len-1
//2.记录当前的第一个位置
for(int k = 0; k < len - 1 ;k++)
{
int min = k;
//必须全部遍历,找最小位置
for(int i = k;i < len;i++)
{
if(arr[min] > arr[i])
{
min = i;
}
}
SwapData(&arr[k],&arr[min]);
}
}
效率分析:
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
直接插入排序
算法思路:
将待排序数据分为两部分,左部分为已经排序好的数据,右部分为待排序数据,从右边的数据中取一个数,插入到左边,并且使左边部分数据依旧有序。
void Insert(int *arr,int len)
{
for(int i = 1;i < len;i++)
{
int tmp = arr[i];
int j = i - 1;
for(;j >= 0 && arr[j] > tmp ;j--)
{
arr[j+1] = arr[j];
}
arr[j+1] = tmp;
}
}
效率分析:
时间复杂度:
最坏情况(j从i跑到-1):O(n^2)
平均情况:O(n^2)
最好情况(数据完全有序):O(n)
空间复杂度:O(1)
稳定性:稳定
使用直接插入排序算法对一组有序的数据进行排序,则时间复杂度趋于O(n)
希尔排序
算法思路:
将待排序数据分组,然后直接使用直接插入排序,在分组内进行排序,组数越来越少,使整个待排序数据趋于有序。
void Shell(int *arr, int len ,int group)
{
for(int i = group;i < len ; i++ )
{
int tmp = arr[i];
int j = i - group;
for(;j >= 0 && arr[j] > tmp ;j = j-group)
{
arr[j+group] = arr[j];
}
arr[j+group] = tmp;
}
}
void ShellSort(int *arr, int len)
{
int group[] = {5,3,1};
for(int i = 0; i < sizeof(group)/sizeof(int) ;i++)
{
Shell(arr, len ,group[i]);
}
}
时间复杂度::O(n^1.3 ———— n^1.5)
空间复杂度:O(d) ————存储增量序列
稳定性:不稳定
堆排序
最大堆(大根堆):这一棵(完全)二叉树上,每个子树上父节点的犬只都要大于左,右孩子;根结点是最大的权值。
这里二叉树我们用顺序结构实现。
规律:
父节点下标为i;
则左孩子的下标为2*i+1;
则右孩子的下标为2*i+2;
若一个孩子的下标为i;
则其父节点的下标为:(i-1)/2;
初始建立大根堆有两个注意点:
- 调整必须从最后一棵子树开始,最后一棵子树的根结点:
(len-1-1)/2=(len-2)/2
- 每一棵子树的调整都是从当前子树的根结点开始调整
算法思路:
先将待排序数据调整成一棵大根堆,将大根堆根结点和当前最后结点交换,然后将剩余的结点调整成大根堆。重复此过程,直到只剩下一个节点。
//在root位置调整这一颗子树,调整这棵树为大根堆
void Adjust(int *arr,int len,int root)
{
int i = root;
int j = 2 * i +1;//左孩子
int tmp = arr[root];
while(j < len)
{
//j指向较大的一个
if(j < len - 1 && arr[j+1] > arr[j])j++;//有右孩子且右孩子比左孩子大
if(arr[j] < tmp)
{
break;
}
arr[i] = arr[j];
i = j;
j = 2*i+1;
}
arr[i] = tmp;
}
//O(nlogn)
void CreateHeap(int *arr,int len)
{
//最后一棵子树的根结点
int root = (len-2)/2;
//循环控制调整每一棵子树,root是当前需要调整的子树的根结点
for(;root >= 0;root--)
{
Adjust(arr,len,root);
}
}
void HeapSort(int *arr,int len)
{
//大根堆构建完成
CreateHeap(arr,len);
for(int i = 0;i < len - 1;i++)
{
SwapData(&arr[0],&arr[len-1-i]);
Adjust(arr,len - 1 - i,0);
}
}
时间复杂度::O(logn)最稳定的时间复杂度
空间复杂度:O(1)
稳定性:不稳定
快速排序
算法思路:
- 在待排序数据中选取一个数据作为基准(第一个);
- 使用基准数据将剩余的数据分成两部分,左部分(不一定有序)都比基准小,右部分(不一定有序)都比基准大;
- 分别再对左右两部分(至少有两个数据)进行快速排序(递归);
前两步封装成函数:QuickOne();一次快排
第三步是快排的接口:Quick();参数arr,start,end
接口封装:QuickSort();参数arr,len
//O(n)
int QuickOne(int *arr,int start,int end)
{
int tmp = arr[start];
int i = start;
int j = end;
//所有数据被遍历了一遍
while(i < j)
{
//从后向前找比tmp小的数据
while(i < j && arr[j] >= tmp)
{
j--;
}
arr[i] = arr[j];
//从前往后找比tmp大的数据
while(i < j && arr[i] <= tmp)
{
i++;
}
arr[j]= arr[i];
}
arr[i] = tmp;
return i;
}
//O(nlogn)
void Quick(int *arr,int start,int end)
{
int mid = QuickOne(arr,start,end);
//O(logn)
//mid左部分
if(mid - start >1)
{
Quick(arr,start,mid-1);
}
//mid右部分
if(end - mid >1)
{
Quick(arr,mid+1,end);
}
}
void QuickSort(int *arr,int len)
{
Quick(arr,0,len - 1);
}
因为递归实现的函数栈空间相对较大,所以在相同的数据量下,递归的绝对空间消耗比较大。
快排的非递归实现:
使用自己开辟的空间将一次快排后的左部分和右部分的一对下标记录下来(栈)
void Quick2(int *arr,int start,int end)
{
SqStack st;
InitStack(&st);
Push(&st , start);
Push(&st , end);
while(!IsEmpty(&st))
{
int right = Top(&st);
Pop(&st);
int left = Top(&st);
Pop(&st);
int mid = QuickOne(arr,left,right);
if(mid - left >1)
{
Push(&st,left);
Push(&st,mid - 1);
}
if(right - mid >1)
{
Push(&st,mid+1);
Push(&st,right);
}
}
DestroyStack(&st);
}
时间复杂度::O(nlogn)
…最坏(已有序数据123456)O(n^2)
空间复杂度:O(logn) ——递归涉及到函数栈的开辟
…最坏(已有序数据123456)O(n)
稳定性:不稳定
快排优化需要注意的问题
-
基准的选取
-
①随机法选取->随机选择一个位置,然后将其和第一个位置的值交换)
-
②三个数取中位数
-
优化
-
①当待排序序列的长度分割到一定大小后,使用插入排序。
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排。
截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生类似的结果,这种做法也避免了一些有害的退化情形。摘自《数据结构与算法分析》Mark Allen Weiness 著 -
②在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
举例:
待排序序列 1 4 6 7 6 6 7 6 8 6
三数取中选取枢轴:下标为4的数6
转换后,待分割序列:6 4 6 7 1 6 7 6 8 6
枢轴key:6
本次划分后,未对与key元素相等处理的结果:1 4 6 6 7 6 7 6 8 6
下次的两个子序列为:1 4 6 和 7 6 7 6 8 6
本次划分后,对与key元素相等处理的结果:1 4 6 6 6 6 6 7 8 7
具体过程:在处理过程中,会有两个步骤
第一步,在划分过程中,把与key相等元素放入数组的两端
第二步,划分结束后,把与key相等的元素移到枢轴周围
举例:
待排序序列 1 4 6 7 6 6 7 6 8 6
三数取中选取枢轴:下标为4的数6
转换后,待分割序列:6 4 6 7 1 6 7 6 8 6
枢轴key:6
第一步,在划分过程中,把与key相等元素放入数组的两端
结果为:6 4 1 6(枢轴) 7 8 7 6 6 6
此时,与6相等的元素全放入在两端了
第二步,划分结束后,把与key相等的元素移到枢轴周围
结果为:1 4 66(枢轴) 6 6 6 7 8 7
此时,与6相等的元素全移到枢轴周围了
之后,在1 4 和 7 8 7两个子序列进行快排
从一个工程中引入另外一个工程的方法:
-
引入另一个工程的头文件
#include "../栈(顺序表实现)/stack.h"
(只引入了方法和结构) -
将被引入工程改为静态库
-
将本工程添加引用
归并排序
算法思路:初始时,认为每一个单独数据都是有序的,将相邻的两个区间段数据(各自区间段的数据都是有序的)归并到一块,并且使归并的结果有序,重复上述过程,直到仅剩下一个段的数据。
//O(n)
void Meger(int *arr, int *brr, int len, int width)
{
int index = 0;//额外空间brr下标的控制
//归并段1
int low1 = 0;
int high1 = low1 + width - 1;
//归并段2
int low2 = high1 + 1;
int high2 = low2 + width - 1 < len ? low2 + width - 1 : len - 1;
//归并处理arr中所有的归并段,while只能处理有两个相邻归并段的情况
while (low2 <len)
{
//两个归并段都有数据未被归并
while (low1 <= high1 && low2 <= high2)
{
if (arr[low1] < arr[low2])
{
brr[index++] = arr[low1++];
}
else
{
brr[index++] = arr[low2++];
}
}
//仅有一个归并段有数据
while (low1 <= high1)
{
brr[index++] = arr[low1++];
}
while (low2 <= high2)
{
brr[index++] = arr[low2++];
}
low1 = high2 + 1;
high1 = low1 + width - 1;
low2 = high1 + 1;
high2 = low2 + width - 1 < len ? low2 + width - 1 : len - 1;
}
//整个arr中剩下最后一个单独的归并段
while (low1 < len)
{
brr[index++] = arr[low1++];
}
}
//一次归并过程
void OneMeger(int* arr, int len,int width)//归并段数据个数
{
//brr用于缓存归并结果
int *brr = (int*)malloc(len * sizeof(int));
assert(brr != NULL);
//归并过程
Meger(arr, brr, len, width);
//将brr中的数据复制回arr中并销毁brr的空间
for (int i = 0; i < len; i++)
{
arr[i] = brr[i];
}
free(brr);
}
void MegerSort(int* arr, int len)
{
int width = 1;
while (width < len)
{
OneMeger(arr, len, width);
width *= 2;
}
}
时间复杂度::O(nlogn)
空间复杂度:O(n)
稳定性:稳定
基数排序
算法思路:按照关键字进行排序
int GetMaxWidth(int *arr, int len)
{
int max = 0;
for (int i = 0; i < len; i++)
{
if (arr[i] > max)
{
max = arr[i];
}
}
int width = 0;
while (width)
{
width++;
max /= 10;
}
}
int GetNum(int data, int width)
{
int num = data % 10;
while (width)
{
data /= 10;
num = data % 10;
width--;
}
return num;
}
//根据width的值所代表的位数进行排序0个位1十位。。。。
void Radix(int* arr, int len, int width)
{
SeQueue que[10];
for (int i = 0; i < 10; i++)
{
InitQue(&que[i]);
}
//扫描arr中的元素,根据width的值获取的相应位数值将其Push到相应的队列中
for (int i = 0; i < len; i++)
{
int num = GetNum(arr[i], width);
Push(&que[num], arr[i]);
}
//将队列中的所有数据获取出来,存储到arr中
int index = 0;
for (int i = 0; i < 10; i++)
{
while (!IsEmpty(&que[i]))
{
Pop(&que[i], &arr[index++]);
}
DestroyQue(&que[i]);
}
}
void RadixSort(int *arr, int len)
{
int width = GetMaxWidth(arr, len);
for (int i = 0; i < width; i++)
{
Radix(arr, len, i);
}
}
时间复杂度::O(k*n)最稳定的时间复杂度
空间复杂度:O(k+n)
稳定性:稳定
引言
学习如何产生随机数,利用产生的随机数测试各种排序算法的实现。
伪随机数
种子在每次启动计算机时是随机的,但是一旦计算机启动以后它就不再变化了;也就是说,每次启动计算机以后,种子就是定值了,所以根据公式推算出来的结果(也就是生成的随机数)就是固定的。引入头<stdlib.h>。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = rand();
printf("%d\n",a);
return 0;
}
解决随机数固定的方法
它需要一个 unsigned int 类型的参数。在实际开发中,我们可以用时间作为参数,只要每次播种的时间不同 (随机因子不同),那么生成的种子就不同,最终的随机数也就不同。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
srand((unsigned)time(NULL));
int a;
a = rand();
printf("%d\n", a);
return 0;
}
为了实现以下几种算法:封装以下的功能:
显示
void ShowData(int *arr,int len)
{
for(int i = 0;i < len ;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
交换
void SwapData(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
主函数
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define DATANUM 15
int main()
{
srand((unsigned)time(NULL));
int arr[DATANUM];
for(int i = 0; i < DATANUM ; i++)
{
arr[i]= rand() % 100;
}
ShowData(arr,DATANUM);
//引用的排序算法
ShowData(arr,DATANUM);
return 0;
}
排序算法总结
- 任何借助“比较”的排序算法,至少需要O ( nlogn )空间
- 记录本身信息量较大时,用链表作为存储结构
- 排序趟数与原始状态无关:直接插入、简单选择、基数
- 排序中比较次数的数量级与序列初始状态无关:简单选择、归并
稳定性
- 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
- 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
选择排序算法准则
- 待排序的记录数目n的大小;
- 记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小
- 关键字的结构及其分布情况;
- 对排序稳定性的要求。
针对n的大小选择不同排序算法
- 当n较大,则应采用时间复杂度为O ( n ∗ l o g n )
O(n*logn)O(n∗logn)的排序方法:快速排序、堆排序或归并排序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序:如果内存空间允许且要求稳定性的;
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
- 当n较大,内存空间允许,且要求稳定性:归并排序
- 当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序:当元素分布有序,如果不要求稳定性,选择直接选择排序。
- 一般不使用或不直接使用传统的冒泡排序。
- 基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解;
2、记录的关键字位数较少,如果密集更好;
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序.