算法概念
1.算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
2.相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数
3.算法复杂度
排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性 插入排序 稳定 希尔排序 稳定 选择排序 稳定 冒泡排序 稳定 快速排序 稳定 堆排序 稳定 归并排序 稳定 计数排序 稳定 桶排序 稳定 基数排序 稳定
十种算法
一、插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法描述
- 一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
算法演示
代码实现
- (NSArray *)insertionSort:(NSArray *)arr { NSMutableArray *array = [[NSMutableArray alloc] initWithArray:arr]; int preIndex = 0; id current; for (int i = 1; i < array.count; i++) { preIndex = i - 1; current = array[I]; while (preIndex >= 0 && array[preIndex] > current) { array[preIndex + 1] = array[preIndex]; preIndex--; } array[preIndex + 1] = current; } return array; }
算法分析
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
二、希尔排序(Shell Sort)
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
算法描述
增量gap的范围: 1<= gap < 待排序数组的长度 (gap 需为 int 值)
增量的取值: 一般的初次取序列(数组)的一半为增量,以后每次减半,直到增量为1。
第一个增量=数组的长度/2,
第二个增量= 第一个增量/2,
第三个增量=第二个增量/2,
以此类推,最后一个增量=1。
- 根据增量gap将数组分成若干组;
- 分别对每个增量序列进行插入排序;
- 重复步骤1~2,直至gap=1。
算法演示
代码实现
- (NSArray *)ShellSort:(NSArray *)arr { NSMutableArray *array = [[NSMutableArray alloc] initWithArray:arr]; int len = (int)array.count; id temp; int gap = len / 2; while (gap > 0) { for (int i = gap; i < array.count; i++) { temp = array[I]; int preIndex = i - gap; while (preIndex >= 0 && array[preIndex] > temp) { array[preIndex + gap] = array[preIndex]; preIndex -= gap; } array[preIndex + gap] = temp; } gap /= 2; } return array; }
算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
三、选择排序(Selection Sort)
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。
- 初始状态:无序区为R[1..n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了
算法演示
代码实现
- (NSArray *)selectionSort:(NSArray *)arr { NSMutableArray *array = [[NSMutableArray alloc] initWithArray:arr]; int len = (int)array.count; int minIndex; id temp; for (int i = 0; i < len - 1; i++) { minIndex = I; for (int j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { // 寻找最小的数 minIndex = j; // 将最小数的索引保存 } } temp = arr[i]; array[i] = arr[minIndex]; array[minIndex] = temp; } return arr; }
算法分析
表现最稳定的排序算法之一,因为无论什么数据进去都是的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
四、冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
算法演示
代码实现
- (NSArray *)bubbleSort:(NSArray *)arr { NSMutableArray *array = [[NSMutableArray alloc] initWithArray:arr]; int len = (int)array.count; for (int i = 0; i < len - 1; i++) { for (int j = 0; j < len - 1 - i; j++) { if (array[j] > array[j+1]) { // 相邻元素两两对比 id temp = array[j+1]; // 元素交换 array[j+1] = array[j]; array[j] = temp; } } } return arr; }
算法分析
程序员第一排序,你懂得~
五、快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法演示
代码实现
@property (nonatomic, strong) NSMutableArray *array; - (NSArray *)quickSort:(NSArray *)arr left:(int)left right:(int)right { self.array = [[NSMutableArray alloc] initWithArray:arr]; if (left < right) { int partitionIndex = [self partitionLeft:left right:right]; [self quickSort:self.array left:left right:partitionIndex-1]; [self quickSort:self.array left:partitionIndex+1 right:right]; } return self.array; } - (int)partitionLeft:(int)left right:(int)right { int pivot = left; // 设定基准值(pivot) int index = pivot + 1; for (int i = index; i <= right; i++) { if (self.array[i] < self.array[pivot]) { [self swap:i j:index]; index++; } } [self swap:pivot j:index-1]; return index-1; } - (void)swap:(int)i j:(int)j { id temp = self.array[I]; self.array[i] = self.array[j]; self.array[j] = temp; }
六、堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
算法描述
将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区。
1.1.假设给定无序序列结构如下:
1.2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
1.3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
1.4.这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
将堆顶元素9和末尾元素4进行交换
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
3.1.重新调整结构,使其继续满足堆定义。
3.2.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8。
3.3.后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序。
算法演示
代码实现
- (NSArray *)HeapSort:(NSArray *)arr { NSMutableArray *array = [NSMutableArray arrayWithArray:arr]; // 初始化一个终止计算结点索引位置(一次排序后把最大值移动到数组计算结点范围的末尾) int endIndex = (int)(array.count - 1); while (endIndex > 0) { // 遍历一次数组按照堆的规则排序一次 [self binaryTreeSortWithArray:array maxIndex:endIndex]; // 将建立堆得到的最大根与数组中最后一个数组互换 [array exchangeObjectAtIndex:0 withObjectAtIndex:endIndex--]; } return array; } /// 二叉树规则(大根堆)排序 -- (排序范围为索引为0的位置到待排序结点最大索引位置) - (void)binaryTreeSortWithArray:(NSMutableArray *)array maxIndex:(int)maxIndex { // 从可排序结点的最大索引处开始进行排序 for (int i = maxIndex; i >= 0; i--) { // 获取移动后的子结点的索引 int newIndex = [self compareNodesSizeWithArray:array maxIndex:maxIndex fatherNodeIndex:i]; while (newIndex > 0) { newIndex = [self compareNodesSizeWithArray:array maxIndex:maxIndex fatherNodeIndex:newIndex]; } } } /// 递归比较指定索引位置的结点与其左右子节点的大小 - (int)compareNodesSizeWithArray:(NSMutableArray *)array maxIndex:(int)maxIndex fatherNodeIndex:(int)fatherIndex { // 获取当前索引位置的左右子节点索引 int leftIndex = fatherIndex * 2 + 1; int rightIndex = fatherIndex * 2 + 2; // 该索引和左右子节点值 NSNumber *currentValue = [array objectAtIndex:fatherIndex]; NSNumber *leftValue = nil; NSNumber *rightValue = nil; if (leftIndex <= maxIndex) { leftValue = [array objectAtIndex:leftIndex]; } if (rightIndex <= maxIndex) { rightValue = [array objectAtIndex:rightIndex]; } if (rightValue != nil) { // 右子节点存在, 则左子节点必定存在, 判断三者的值并把最大值换到父节点上 if (currentValue.doubleValue > leftValue.doubleValue && currentValue.doubleValue > rightValue.doubleValue) { return -1; } else { // 与父节点进行交换的结点的索引 int index = (leftValue.doubleValue > rightValue.doubleValue ? leftIndex : rightIndex); [array exchangeObjectAtIndex:fatherIndex withObjectAtIndex: index]; // 移动完毕之后同时还要进行(被移动到子节点的值)与(该被移动节点的左右子节点值)进行比较 -- 递归 return index; } } else if (leftValue != nil) {// 右结点不存在, 左结点存在, 判断左结点是否大于父结点的值 if (currentValue.doubleValue < leftValue.doubleValue) { [array exchangeObjectAtIndex:fatherIndex withObjectAtIndex:leftIndex]; return leftIndex; } else { return -1; } } return -1; }
算法分析
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
七、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
算法演示
代码实现
#import "ViewController.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSMutableArray * array = [NSMutableArray arrayWithObjects:@5,@6,@2,@4,@3,@8,@7,@1, nil]; //调用排序 [self mergeSortArray:array]; NSLog(@"%@", array); // Do any additional setup after loading the view. } - (void)mergeSortArray:(NSMutableArray *)array { //创建一个副本数组 NSMutableArray * auxiliaryArray = [[NSMutableArray alloc]initWithCapacity:array.count]; //对数组进行第一次二分,初始范围为0到array.count-1 [self mergeSort:array auxiliary:auxiliaryArray low:0 high:array.count-1]; } - (void)mergeSort:(NSMutableArray *)array auxiliary:(NSMutableArray *)auxiliaryArray low:(int)low high:(int)high { //递归跳出判断 if (low>=high) { return; } //对分组进行二分 int middle = (high - low)/2 + low; //对左侧的分组进行递归二分 low为第一个元素索引,middle为最后一个元素索引 [self mergeSort:array auxiliary:auxiliaryArray low:low high:middle]; //对右侧的分组进行递归二分 middle+1为第一个元素的索引,high为最后一个元素的索引 [self mergeSort:array auxiliary:auxiliaryArray low:middle + 1 high:high]; //对每个有序数组进行回归合并 [self merge:array auxiliary:auxiliaryArray low:low middel:middle high:high]; } - (void)merge:(NSMutableArray *)array auxiliary:(NSMutableArray *)auxiliaryArray low:(int)low middel:(int)middle high:(int)high { //将数组元素复制到副本 for (int i=low; i<=high; i++) { auxiliaryArray[i] = array[I]; } //左侧数组标记 int leftIndex = low; //右侧数组标记 int rightIndex = middle + 1; //比较完成后比较小的元素要放的位置标记 int currentIndex = low; while (leftIndex <= middle && rightIndex <= high) { //此处是使用NSNumber进行的比较,你也可以转成NSInteger再比较 if ([auxiliaryArray[leftIndex] compare:auxiliaryArray[rightIndex]]!=NSOrderedDescending) { //左侧标记的元素小于等于右侧标记的元素 array[currentIndex] = auxiliaryArray[leftIndex]; currentIndex++; leftIndex++; } else { //右侧标记的元素小于左侧标记的元素 array[currentIndex] = auxiliaryArray[rightIndex]; currentIndex++; rightIndex++; } } //如果完成后左侧数组有剩余 if (leftIndex <= middle) { for (int i = 0; i<=middle - leftIndex; i++) { array[currentIndex +i] = auxiliaryArray[leftIndex +I ]; } } } @end
算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
八、计数排序(Counting Sort)
计数排序 的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。
算法描述
1.找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
算法演示
代码实现
- (NSArray *)countSort:(NSArray *)arr { int maxValue = 0; for (NSNumber *num in arr) { if (num.intValue > maxValue) { maxValue = num.intValue; } } NSMutableArray *array = [NSMutableArray array]; int arrLen = (int)arr.count; NSMutableDictionary<NSString *,NSNumber *> *bucket = [NSMutableDictionary dictionary]; int sortedIndex = 0; int bucketLen = maxValue + 1; for (int i = 0; i < arrLen; i++) { NSString *key = [NSString stringWithFormat:@"%@", arr[I]]; if (!bucket[key]) { bucket[key] = @0; } NSNumber *num = bucket[key]; bucket[key] = @(num.intValue + 1); } for (int j = 0; j < bucketLen; j++) { NSString *key = [NSString stringWithFormat:@"%d", j]; while(bucket[key].intValue > 0) { array[sortedIndex ++] = @(j); bucket[key] = @(bucket[key].intValue - 1); } } return array; }
算法分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
九、桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
算法演示
代码实现
- (NSArray *)bucketSort:(NSArray *)arr{ NSMutableArray *array = [NSMutableArray arrayWithArray:arr]; //预计每个桶内能装3个 NSInteger size = 3; //桶的数量 NSInteger bucketsCount = array.count / size; if ((array.count % size) > 0) { bucketsCount ++; } //找出最小值和最大值 NSInteger min = [array[0] integerValue]; NSInteger max = [array[0] integerValue]; for (NSNumber *number in array) { if (number.integerValue < min) { min = number.integerValue; } if (number.integerValue > max) { max = number.integerValue; } } //平均值 NSInteger average = ceil((double)(max - min)/(double)bucketsCount); NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; for (NSInteger i = 0; i < bucketsCount; i++) { NSMutableArray *bucketArray = [NSMutableArray array]; NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)]; [dictionary setValue:bucketArray forKey:key]; } for (NSNumber *number in array) { NSInteger i = floor((double)(number.integerValue - min) / >(double)average); NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)]; NSMutableArray *bucketArray = [dictionary valueForKey:key]; [bucketArray addObject:number]; } NSInteger length = 0; for (NSInteger i = 0; i < dictionary.allKeys.count; i++) { NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)]; NSMutableArray *bucketArray = [dictionary objectForKey:key]; [self mergeSortArray:bucketArray]; [array replaceObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(length, bucketArray.count)] withObjects:bucketArray]; length += bucketArray.count; } return array; }
算法分析
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
十、基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
算法演示
代码实现
- (NSArray *)radixSort:(NSMutableArray *)arr { NSMutableArray *array = [NSMutableArray arrayWithArray:arr]; int len = (int)array.count; NSMutableArray *radixArrays = [NSMutableArray array]; for (int i=0; i<10; i++) { NSMutableArray *arr = [NSMutableArray array]; for (int i=0; i<len; i++) { [arr addObject:@0]; } [radixArrays addObject:arr]; } int KEYNUM_31 = 10; //关键字个数,这里为32位整形位数 for (int pos = 1; pos <= KEYNUM_31; pos++) { for (int i = 0; i < len; i++) { int num = [self GetNumInPos:[array[i] intValue] pos:pos]; int num2 = [radixArrays[num][0] intValue]+1; radixArrays[num][0] = @(num2); radixArrays[num][num2] = array[I]; } for (int i = 0,j = 0; i < 10; i++) { for (int k = 1; k <= [radixArrays[i][0] intValue]; k++) { array[j] = radixArrays[i][k]; j=j+1; } radixArrays[i][0] = @0; //复位 } } return array; } - (int)GetNumInPos:(int)num pos:(int)pos{ int temp = 1; for (int i=0; i<pos-1; i++) { temp *= 10; } return (num/temp)%10; }
算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。