前言
排序算法也算是老生常谈了,如果你大学专业是计算机或软件,甚至你参加过国二国三都会学到排序算法,如果我没猜错的话你接触的第一个算法是冒泡。
排序算法老生常谈,但确实多少大厂面试题中的必考题。
废话不多说,开始正题
常见的八种排序算法他们的关系如下:
排序一:冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
上代码:
/**
冒泡排序
@param array 目标数组
@return 排序后数组
*/
+ (NSArray *)bubblingSorting:(NSMutableArray *)array {
int count = 0;
int forcount = 0;
for (int i = 0; i < array.count; i++) {
forcount++;
for (int j = (int)array.count-2; j >= i; j--) {
count++;
if ([array[j] doubleValue] > [array[j+1] doubleValue]) {
[array exchangeObjectAtIndex:j withObjectAtIndex:j+1];
}
}
}
return array;
}
排序二-选择排序
选择排序(Selection Sort)也是一种简单直观的排序算法。
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
上代码:
/**
选择排序
@param array 目标数组
@return 排序后数组
*/
+ (NSArray *)chooseSort:(NSMutableArray *)array {
for (int i=0; i [array[j] doubleValue]) {
[array exchangeObjectAtIndex:i withObjectAtIndex:j];
}
}
}
return array;
}
排序三-归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
上代码:
/**
归并排序
@param array 目标数组
@return 排序后数组
*/
+ (NSArray *)megerSort:(NSMutableArray *)array {
/**
归并排序其实要做两件事:
(1)“分解”——将序列每次折半划分。
(2)“合并”——将划分后的序列段两两合并后排序。
*/
//排序数组
NSMutableArray *tempArray = [NSMutableArray arrayWithCapacity:1];
//第一趟排序是的子数组个数为ascendingArr.count
for (NSNumber *num in array) {
NSMutableArray *subArray = [NSMutableArray array];
[subArray addObject:num];
[tempArray addObject:subArray];
}
/**
分解操作 每一次归并操作 tempArray的个数为
(当数组个数为偶数时tempArray.count/2;当数组个数为奇数时tempArray.count/2+1)
当tempArray.count == 1时,归并排序完成
*/
while (tempArray.count != 1) {
NSInteger i = 0;
//当数组个数为偶数时 进行合并操作, 当数组个数为奇数时,最后一位轮空
while (i < tempArray.count - 1) {
//将i 与i+1 进行合并操作 将合并结果放入i位置上 将i+1位置上的元素删除
tempArray[i] = [self mergeArrayFirstList:tempArray[i] secondList:tempArray[i + 1]];
[tempArray removeObjectAtIndex:i + 1];
//i++ 继续下一循环的合并操作
i++;
}
}
NSLog(@"归并排序结果:%@", tempArray);
return (NSArray *)tempArray[0];
}
//合并
+ (NSArray *)mergeArrayFirstList:(NSArray *)array1 secondList:(NSArray *)array2 {
// 合并序列数组
NSMutableArray *resultArray = [NSMutableArray array];
// firstIndex是第一段序列的下标 secondIndex是第二段序列的下标
NSInteger firstIndex = 0, secondIndex = 0;
// 扫描第一段和第二段序列,直到有一个扫描结束
while (firstIndex < array1.count && secondIndex < array2.count) {
// 判断第一段和第二段取出的数哪个更小,将其存入合并序列,并继续向下扫描
if ([array1[firstIndex] floatValue] < [array2[secondIndex] floatValue]) {
[resultArray addObject:array1[firstIndex]];
firstIndex++;
} else {
[resultArray addObject:array2[secondIndex]];
secondIndex++;
}
}
// 若第一段序列还没扫描完,将其全部复制到合并序列
while (firstIndex < array1.count) {
[resultArray addObject:array1[firstIndex]];
firstIndex++;
}
// 若第二段序列还没扫描完,将其全部复制到合并序列
while (secondIndex < array2.count) {
[resultArray addObject:array2[secondIndex]];
secondIndex++;
}
// 返回合并序列数组
return resultArray.copy;
}
排序四:快速排序
快速排序(Quick Sort)是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
算法步骤:
- 从数列中挑出一个元素,称为 “基准”(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
上代码:
/**
快速排序
@param array 目标数组
@param left 左标记
@param right 右标记
@return 排序后数组
*/
+ (NSArray *)quickSortArray:(NSMutableArray *)array
leftIndex:(NSInteger)left
rightIndex:(NSInteger)right {
if (left > right) {
return @[];
}
NSInteger i = left;
NSInteger j = right;
//记录基准数 pivoty
id key = array[i];
while (i < j) {
//首先从右边j开始查找(从最右边往左找)比基准数(key)小的值<---
while (i < j && [key doubleValue] <= [array[j] doubleValue]) {
j--;
}
//如果从右边j开始查找的值[array[j] integerValue]比基准数小,则将查找的小值调换到i的位置
if (i < j) {
array[i] = array[j];
}
//从i的右边往右查找到一个比基准数小的值时,就从i开始往后找比基准数大的值 --->
while (i < j && [array[i] doubleValue] <= [key doubleValue]) {
i++;
}
//如果从i的右边往右查找的值[array[i] integerValue]比基准数大,则将查找的大值调换到j的位置
if (i < j) {
array[j] = array[i];
}
}
//将基准数放到正确的位置,----改变的是基准值的位置(数组下标)---
array[i] = key;
//递归排序
//将i左边的数重新排序
[self quickSortArray:array leftIndex:left rightIndex:i - 1];
//将i右边的数重新排序
[self quickSortArray:array leftIndex:i + 1 rightIndex:right];
return array;
}
排序五:插入排序
插入排序(Insertion Sort)是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
上代码:
/**
插入排序
@param array 目标数组
@return 排序后数组
*/
+ (NSArray *)inserSort:(NSMutableArray *)array {
//插入排序的原理:始终定义第一个元素为有序的,将元素逐个插入到有序排列之中,其特点是要不断的
//移动数据,空出一个适当的位置,把待插入的元素放到里面去。
for (int i = 0; i < array.count; i++) {
id temp = array[i];
//temp 为待排元素 i为其位置 j为已排元素最后一个元素的位置(即取下一个元素,在已经排好序的元素序列中从后向前扫描)
int j = i-1;
//当j < 0 时, i 为第一个元素 该元素认为已经是排好序的 所以不进入while循环
// [array[j] compare:temp] == NSOrderedDescending与[array[j] intValue] > [temp intValue] 作用相同
while (j >= 0 && [array[j] doubleValue] > [temp doubleValue]) {
//如果已经排好序的序列中元素大于新元素,则将该元素往右移动一个位置
[array replaceObjectAtIndex:j+1 withObject:array[j]];
j--;
}
//跳出while循环时,j的元素小于或等于i的元素(待排元素)。插入新元素 i= j+1
[array replaceObjectAtIndex:j+1 withObject:temp];
NSLog(@"插入排序排序中:%@",array);
}
return array;
}
排序六:希尔排序
希尔排序(Shell Sort)也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法步骤:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
上代码:
/**
希尔排序
@param array 目标数组
@return 排序后数组
*/
+ (NSArray *)shellSort:(NSMutableArray *)array {
int gap = (int)array.count / 2;
while (gap >= 1) {
for(int i = gap ; i < [array count]; i++) {
id temp = array[i];
int j = i;
while (j >= gap && [temp doubleValue] < [[array objectAtIndex:(j - gap)] doubleValue]) {
[array replaceObjectAtIndex:j withObject:[array objectAtIndex:j-gap]];
j -= gap;
}
[array replaceObjectAtIndex:j withObject:temp];
}
gap = gap / 2;
}
return array;
}
排序七-堆排序
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的平均时间复杂度为Ο(nlogn) 。
算法步骤:
- 创建一个堆H[0..n-1]
- 把堆首(最大值)和堆尾互换
- 把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
- 重复步骤2,直到堆的尺寸为1
上代码:
/**
堆排序
@param array 目标数组
@return 排序后数组
*/
+ (NSArray *)heapSort:(NSMutableArray *)array {
NSInteger i ,size;
size = array.count;
//找出最大的元素放到堆顶
for (i= array.count/2-1; i>=0; i--) {
[self createBiggesHeap:array withSize:size beIndex:i];
}
while(size > 0){
[array exchangeObjectAtIndex:size-1 withObjectAtIndex:0]; //将根(最大) 与数组最末交换
size -- ;//树大小减小
[self createBiggesHeap:array withSize:size beIndex:0];
}
NSLog(@"%@",array);
return array;
}
+ (void)createBiggesHeap:(NSMutableArray *)list withSize:(NSInteger)size beIndex:(NSInteger)element {
NSInteger lchild = element *2 + 1,rchild = lchild+1; //左右子树
while (rchild < size) { //子树均在范围内
if ([list[element] doubleValue] >= [list[lchild] doubleValue] && [list[element] doubleValue] >= [list[rchild] doubleValue]) return; //如果比左右子树都大,完成整理
if ([list[lchild] doubleValue] > [list[rchild] doubleValue]) { //如果左边最大
[list exchangeObjectAtIndex:element withObjectAtIndex:lchild]; //把左面的提到上面
element = lchild; //循环时整理子树
}else{//否则右面最大
[list exchangeObjectAtIndex:element withObjectAtIndex:rchild];
element = rchild;
}
lchild = element * 2 +1;
rchild = lchild + 1; //重新计算子树位置
}
//只有左子树且子树大于自己
if (lchild < size && [list[lchild] integerValue] > [list[element] integerValue]) {
[list exchangeObjectAtIndex:lchild withObjectAtIndex:element];
}
}
排序八-基数排序
基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
说基数排序之前,我们简单介绍桶排序:
算法思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使 用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)10, i10]的整数,i = 1,2,..100。总共有 100个桶。
然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果
对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
- 首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
- 其次待排序的元素都要在一定的范围内等等。
上代码:
/**
基数排序
@param array 目标数组
@return 排序后的array
*/
+ (NSArray *)radixSort:(NSMutableArray *)array {
NSMutableArray *bucket = [self createBucket];
double maxNumber = [self listMaxItem:array];
NSInteger maxLength = [self numberLength:maxNumber];
for (NSInteger digit = 1; digit <= maxLength; digit++) {
//入桶
for (NSNumber *number in array) {
NSInteger baseNumber = [self fetchBaseNumber:number.doubleValue digit:digit];
NSMutableArray *subArray = bucket[baseNumber];
[subArray addObject:number];
}
//出桶
NSInteger index = 0;
for (NSInteger i = 0; i < bucket.count; i++) {
NSMutableArray *subArray = bucket[i];
while (subArray.count > 0) {
NSNumber *item = subArray[0];
[array replaceObjectAtIndex:index withObject:item];
[subArray removeObjectAtIndex:0];
index++;
}
}
}
return array;
}
//创建10个空桶
+ (NSMutableArray *)createBucket {
NSMutableArray *bucketArray = [NSMutableArray array];
for (NSInteger i = 0; i < 10; i++) {
[bucketArray addObject:[NSMutableArray array]];
}
return bucketArray;
}
//计算无序序列中最大的数值
+ (double)listMaxItem:(NSArray *)array {
double maxNumber = [array[0] doubleValue];
for (NSNumber *number in array) {
if (maxNumber < number.doubleValue) {
maxNumber = number.doubleValue;
}
}
return maxNumber;
}
//获取数字的长度
+ (NSInteger)numberLength:(double)number {
NSString *numberStr = [NSString stringWithFormat:@"%f",number];
return numberStr.length;
}
//获取数值中特定位数的值
+ (NSInteger)fetchBaseNumber:(double)number digit:(NSInteger)digit {
if (digit > 0 && digit <= [self numberLength:number]) {
NSMutableArray *numbersArray = [NSMutableArray array];
NSString *numberStr = [NSString stringWithFormat:@"%f",number];
for (NSInteger i = 0; i < numberStr.length; i++) {
NSString *subStr = [numberStr substringWithRange:NSMakeRange(i, 1)];
[numbersArray addObject:[NSNumber numberWithInteger:subStr.doubleValue]];
}
return [numbersArray[numbersArray.count - digit] integerValue];
}
return 0;
}
总结
各种排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图:
关于时间复杂度:
平方阶(O(n2))排序:
各类简单排序:直接插入、直接选择和冒泡排序;线性对数阶(O(nlog2n))排序:
快速排序、堆排序和归并排序O(n1+§))排序,§是介于0和1之间的常数:
希尔排序线性阶(O(n))排序:
基数排序,此外还有桶、箱排序。
关于时间复杂度:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序