目录
一、冒泡排序
冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序。
代码实现:
void Swap(int *a,int *b)//交换,交换函数在下面的排序中不再写出
{
int c = *a;
*a = *b;
*b = c;
}
// 时复O(n^2) 空复O(1) 稳定
void BubbleSort(int *arr, int len)
{
int i = 0;
int j = 0;
bool flag = true;//flag用来做标记,初始为真,如果没有数据交换,flag为真
for (i = 0; i < len - 1; i++)//第几趟,最后一趟的时候最后的数已经排好序了,所以i < len-1
{
flag = true;
for (j = 0; j < len - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
flag = false;//如果有数据交换,flag为假
}
}
if (flag)//表示此次循环没有进行交换,也就是待排序列已经有序,排序已完成 break
{
break;
}
}
}
根据上面这种冒泡实现,若原数组本身就是有序的(这是最好情况),仅需n-1次比较就可完成;
若是倒序,比较次数为 n-1+n-2+...+1=n(n-1)/2,交换次数和比较次数等值。
所以,其时间复杂度依然为O(n2)。
时复最好情况O(n),最坏、平均O(n^2) ;空复O(1) ;稳定
二、简单选择排序
基本思想为每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。
在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。
其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。
我们可以通过设置一个变量min,每一次比较仅存储较小元素的数组下标,
当一轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。
代码实现:
// O(n^2) O(1) 不稳定
void SelectSort(int *arr,int len)
{
int i = 0;
int min = 0;
for( i; i < len - 1; ++i )//i<len-1是因为循环在最后一次完成时,数组中的最后一个数肯定是最大的,不用再进行一次比较
{
int min = i;
//每一趟循环比较时,min用于存放较小元素的数组下标,这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,避免每次遇到较小元素都要进行交换。
for( int j = i + 1; j < len; ++j )
{
if( arr[j] < arr[min] )
{
min = j;//将较小的元素下标赋给min,循环走完后min中将存放的是较小的元素下标
}
}
if( min != i )//如果min的值不再等于i,则交换
{
Swap(&arr[i],&arr[min]);
}
}
}
无论最好情况还是最坏情况,比较次数一样多,只不过不用交换,时复O(n^2);空复O(1);不稳定
三、直接插入排序
基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。(类似于扑克牌整牌)
代码实现:
void InsertSort(int *arr,int len)
{
if (arr == NULL || len < 2)
{
return;
}
int i;
for(i = 1;i < len;i++)
{
int j = i - 1;
for (j; j >= 0 && arr[j] > arr[j + 1]; j--)
{
Swap(&arr[j], &arr[j + 1]);
}
}
}
简单插入排序在最好情况下,需要比较n-1次,无需交换元素,时间复杂度为O(n);
在最坏情况下,时间复杂度依然为O(n^2)。
但是在数组元素随机排列的情况下,插入排序还是要优于冒泡和简单选择两种排序的。
时复O(n^2);空复O(1);稳定
四、归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
代码实现:
//递归方法
void MergeDG(int *arr,int L,int M,int R)
{
int *brr = (int*)malloc(sizeof(int)*(R-L+1));//长度是R-L+1
assert(brr != NULL);
int i = 0;
int p1 = L;
int p2 = M+1;
while(p1 <= M && p2 <= R)
{
if(arr[p1] < arr[p2])
{
brr[i++] = arr[p1++];
}
else
{
brr[i++] = arr[p2++];
}
}
while(p1 <= M)
{
brr[i++] = arr[p1++];
}
while(p2 <= R)
{
brr[i++] = arr[p2++];
}
for(i = 0;i < R-L+1;i++)//将brr中的已经排好序的数组元素利用for循环放入到arr中
{
arr[L+i] = brr[i];//从L+i位置开始,不是从i位置开始
}
free(brr);
}
void MergeSortDG(int *arr,int L,int R)
{
if(L == R)//数组中只有一个数据
{
return;
}
int mid = ((R-L)>>1) + L;//mid = (L+R)/2 两者相加容易越界 ==》(R-L)/2 + L ==》(R-L)>>1 + L 位运算比算术运算快;右移相当于原来的数除2,左移相当于原来的数乘2
MergeSortDG(arr,L,mid);
MergeSortDG(arr,mid+1,R);
MergeDG(arr,L,mid,R);
}
时间复杂度为O(nlogn),空间复杂度为O(n),稳定
归并要进行logn次,类似于二叉树的深度
五、快速排序
基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,
然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
快速排序是一种交换类的排序,它同样是分治法的经典体现。在一趟排序中将待排序的序列分割成两组,其中一部分记录的关键字均小于另一部分。然后分别对这两组继续进行排序,以使整个序列有序。在分割的过程中,枢纽值的选择至关重要,采取了三位取中法,
可以很大程度上避免分组"一边倒"的情况。快速排序平均时间复杂度也为O(nlogn)级。
代码实现:
//处理枢纽值(三数取中法,将取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值)
void DealKey(int *arr,int left,int right)
{
int mid = ((right-left)>>1) + left;//(right+left)/2
if (arr[left] > arr[mid])
{
Swap(&arr[left],&arr[mid]);
}
if (arr[left] > arr[right])
{
Swap(&arr[left],&arr[right]);
}
if (arr[right] < arr[mid])
{
Swap(&arr[left],&arr[mid]);
}
Swap(&arr[right - 1],&arr[mid]);//将枢纽值放在最后一个数的前一个
}
void QuickSort(int *arr,int left,int right)
{
if(left < right)
{
DealKey(arr, left, right);//获取枢纽值,并将其放在当前待处理序列末尾
int pivot = right - 1;//枢纽值被放在序列末尾
int i = left;//左指针
int j = right - 1;//右指针
while (left < right)
{
while (arr[i] <= arr[pivot]) //先从左边开始扫描,找出比枢纽值pivot大的值;若该值比pivot小,继续往后走
{
i++;
}
while (j > left && arr[j] >= arr[pivot]) //之后从右边开始扫描,找出比pivot小的值;若该值比pivot大,继续往前走
{
j--;
}
if (i < j) //此时,i对应的值比pivot大,j对应的值比pivot小;若i<j,两者交换,否则跳出循环
{
Swap(&arr[i], &arr[j]);
}
else //i >= j
{
break;
}
}
if(i < right)
{
Swap(&arr[i], &arr[right - 1]);
}
QuickSort(arr, left, i - 1);
QuickSort(arr, i + 1, right);
}
}
如果数组非常小,直接插入排序更好(直接插入排序是简单排序中性能最好的),因为快排用了递归。
在大量数据进行排序的时候,用快排(递归的影响对于整体算法而言是可以忽略的)
时复:最好、平均O(nlogn),最坏O(n^2);空复:O(logn)~O(n);不稳定
六、堆排序
堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,
反复执行调整+交换步骤,直到整个序列有序。
代码实现:
void AdjustHeap(int *arr,int len,int start)//调整大顶堆
{
int tmp = arr[start];//将顶拿出来放到tmp中
int i = 2 * start + 1;//从父节点的左孩子开始
while( i < len )//从左至右,从下至上调整
{
if( i+1 < len && arr[i] < arr[i+1] ) //若左孩子<右孩子,则将i指向右孩子
{
i = i + 1;
}
if( arr[i] > tmp )//如果值大的那个子节点>父节点,将子节点值赋给父节点(不用进行交换)
{
arr[start] = arr[i];
start = i;
}
else
{
break;
}
i = 2 * start + 1;//下一个结点的左孩子
}
arr[start] = tmp;//将tmp中之前顶的值赋给右孩子之前在的位置
}
// 构造初始堆,将给定无序序列构造成一个大顶堆
//(一般升序采用大顶堆,降序采用小顶堆)
void CreatHeap(int *arr,int len)
{
for(int i = (len - 2) / 2 ;i >= 0 ;--i)//从最后一个非叶子节点开始,从下至上,从右至左调整
{
AdjustHeap(arr,len,i);
}
}
//堆排序 O(nlogn) O(1) 不稳定
void HeapSort(int *arr,int len)
{
CreatHeap(arr,len);//先创建一个大顶堆
int end = len - 1;
while( end > 0 )//对大顶堆交换+调整
{
Swap(&arr[0],&arr[end]);//将最后一个元素和顶交换
AdjustHeap(arr,end,0);//调整
end --;
}
}
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。
所以堆排序时间复杂度一般认为就是O(nlogn)级。
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的时间复杂度均为O(nlogn),空复:O(1);不稳定排序。
七、希尔排序
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
在希尔排序的理解时,我们倾向于对于每一个分组,逐组进行处理,但在代码实现中,我们可以不用这么按部就班地处理完一组再调转回来处理下一组(这样还得加个for循环去处理分组)比如[5,4,3,2,1,0] ,首次增量设gap=length/2=3,则为3组[5,2] [4,1] [3,0],实现时不用循环按组处理,我们可以从第gap个元素开始,逐个跨组处理。同时,在插入数据时,可以采用元素交换法寻找最终位置,也可以采用数组元素移动法寻觅。
代码实现:
void ShellSort(int *arr,int len)
{
for( int gap = len/2;gap > 0;gap /= 2 )//从第gap个元素,逐个对其所在组进行直接插入排序操作
{
for( int i = gap;i < len;i++ )
{
int j = i;
if( arr[j] < arr[j-gap] )//同一个组的比大小
{
while( j-gap >= 0 && arr[j] < arr[j-gap] )//交换法
{
Swap(&arr[j],&arr[j-gap]);
j = j-gap;
}
}
}
}
}
希尔排序中对于增量序列的选择十分重要,直接影响到希尔排序的性能。我们上面选择的增量序列{n/2,(n/2)/2...1}(希尔增量),其最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2)。空间复杂度O(1),不稳定
八、基数排序
算法思想:基数排序又称为“桶子法”,从低位开始将待排序的数按照这一位的值放到相应的编号为0~9的桶中。等到低位排完得到一个子序列,再将这个序列按照次低位的大小进入相应的桶中,一直排到最高位为止,数组排序完成。
在实际项目中,如果对效率有所要求,而不太关心空间的使用时,会选择用计数排序(当然还有一些其他的条件),或是一些计数排序的变形。
数据必须全为正数
八种排序算法中唯一不需要数据比较的算法
算法执行步骤:
(1)遍历数组找到最大的数据元素,为的是根据他来求位数
(2)再循环相应位数的值,将数据放入对应的队列中
(3)按照队列顺序将队列中的所有数据输入
代码实现:
typedef struct Que//定义队列,先进先出
{
int *data;
int head;
int tail;
}Que;
//找出数组中的最大值并且求位数 遍历O(n)
int GetWidth(int *arr, int len)
{
int max = arr[0];
for (int i = 1; i < len; ++i)
{
if (arr[i] > max)
{
max = arr[i];
}
}
// 根据max求位数 1234 234 34 4
int width = 0;
while (max > 0)
{
width += 1;
max /= 10;
}
return width;
}
// O(d)
//获取一个数digit位上的数字
int GetRadixVal(int val, int digit) // val = 1234 digit = 0
{
while (digit > 0)
{
val /= 10;
digit--;
}
return val % 10;
}
//O(dn) O(n) 稳定
void RadixSort(int *arr, int len)
{
Que que[10];
for (int i = 0; i < 10; ++i)//开辟了10块空间
{
que[i].data = (int *)malloc(sizeof(int)* len);
assert(que[i].data != NULL);
que[i].head = que[i].tail = 0;
}
int width = GetWidth(arr, len);//获取最大位数
for (int i = 0; i < width; ++i)//按位数循环 个位 十位 百位......
{
for (int j = 0; j < len; ++j)//遍历元素,按位数上所对应的值 分别放入不同的队列中
{
int digitval = GetRadixVal(arr[j], i);//位数上的值
que[digitval].data[que[digitval].tail] = arr[j];
que[digitval].tail++;
}
int count = 0;
for (int k = 0; k < 10; ++k)//将队列的数据放到数组里
{
while (que[k].head != que[k].tail)//如果队列里有数据
{
arr[count++] = que[k].data[que[k].head++];
}
que[k].head = que[k].tail = 0;//将队列的数据清空
}
}
for (int i = 0; i < 10; ++i)
{
free(que[i].data);//释放这10块空间
}
}
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
九、总结
该图来源于:十大经典排序算法(动图演示)
在小样本的情况下(样本<60),用直接插入排序,O(n^2),常数项极低,插排快;
在样本是用户定义的情况下,用归并排序;
在数据类型为基本类型时,用快排。
推荐好的博客:图解排序算法(五)之快速排序——三数取中法