关于算法的想法
由于面试可能需要手写算法,网上搜罗了一些资料,整理了下算法的OC的实现代码,虽然平时开发中一般用不到,但是多积累一些技术知识,还是对以后发展大有裨益的
github上搜集的几大算法原理和实现代码,只有JavaScript、Python、Go、Java的实现代码
算法文字理解和OC代码实现
1. 冒泡排序算法(Bubble Sort)
相邻元素进行比较,按照升序或者降序,交换两个相邻元素的位置 是一种“稳定排序算法”
1.1 网上文字理论
是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
1.2 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.3 动图演示
1.4 什么时候最快
当输入的数据已经是正序时。
1.5 什么时候最慢
当输入的数据是反序时。
1.6 冒泡排序代码示例
- (void)bubbleSortWithArray:(NSMutableArray *)array {
for (int i = 0; i < array.count - 1; i++) {
//外层for循环控制循环次数
for (int j = 0; j < array.count - 1 - i; j++) {
//内层for循环控制交换次数
if ([array[j] integerValue] > [array[j + 1] integerValue]) {
[array exchangeObjectAtIndex:j withObjectAtIndex:j + 1];
}
}
}
}
2. 快速排序算法(quick sort)
2.1 网上文字理解
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案: 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
2.2 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
2.3 动图演示
2.4 快速排序代码示例
- (void)quickSortArray:(NSMutableArray *)array
leftIndex:(NSInteger)left
rightIndex:(NSInteger)right {
if (left > right) {
return;
}
NSInteger i = left;
NSInteger j = right;
//记录基准数 pivoty
NSInteger key = [array[i] integerValue];
while (i < j) {
//首先从右边j开始查找(从最右边往左找)比基准数(key)小的值<---
while (i < j && key <= [array[j] integerValue]) {
j--;
}
//如果从右边j开始查找的值[array[j] integerValue]比基准数小,则将查找的小值调换到i的位置
if (i < j) {
array[i] = array[j];
}
//从i的右边往右查找到一个比基准数小的值时,就从i开始往后找比基准数大的值 --->
while (i < j && [array[i] integerValue] <= key) {
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];
}
3. 选择排序算法(select sort)
它的改进(相比较冒泡算法)在于:先并不急于调换位置,先从A[0]开始逐个检查,看哪个数最小就记下该数所在的位置P,等一躺扫描完毕,再把A[P]和A[0]对调,这时A[0]到A[n]中最小的数据就换到了最前面的位置。是一个“不稳定排序算法”
它是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。
选择排序算法一: 直接选择排序(straight select sort)
3.1 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
3.2 动图演示
3.3 直接选择排序示例代码
- (void)selectSortWithArray:(NSMutableArray *)array {
for (int i = 0; i < array.count; i++) {
for (int j = i + 1; j < array.count; j++) {
if (array[i] > array[j]) {
[array exchangeObjectAtIndex:i withObjectAtIndex:j];
}
}
}
}
选择排序算法二:堆排序(heap sort 涉及到完全二叉树的概念)
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
动图演示
堆排序代码示例
- (void)heapSortWithArray:(NSMutableArray *)array {
//循环建立初始堆
for (NSInteger i = array.count * 0.5; i >= 0; i--) {
[self heapAdjustWithArray:array parentIndex:i length:array.count];
}
//进行n-1次循环,完成排序
for (NSInteger j = array.count - 1; j > 0; j--) {
//最后一个元素和第一个元素进行交换
[array exchangeObjectAtIndex:j withObjectAtIndex:0];
//筛选R[0]结点,得到i-1个结点的堆
[self heapAdjustWithArray:array parentIndex:0 length:j];
NSLog(@"第%ld趟:", array.count - j);
[self printHeapSortResult:array begin:0 end:array.count - 1];
}
}
- (void)heapAdjustWithArray:(NSMutableArray *)array
parentIndex:(NSInteger)parentIndex
length:(NSInteger)length {
NSInteger temp = [array[parentIndex] integerValue]; //temp保存当前父结点
NSInteger child = 2 * parentIndex + 1; //先获得左孩子
while (child < length) {
//如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
if (child + 1 < length && [array[child] integerValue] < [array[child + 1] integerValue]) {
child++;
}
//如果父结点的值已经大于孩子结点的值,则直接结束
if (temp >= [array[child] integerValue]) {
break;
}
//把孩子结点的值赋值给父结点
array[parentIndex] = array[child];
//选取孩子结点的左孩子结点,继续向下筛选
parentIndex = child;
child = 2 * child + 1;
}
array[parentIndex] = @(temp);
}
- (void)printHeapSortResult:(NSMutableArray *)array
begin:(NSInteger)begin
end:(NSInteger)end {
for (NSInteger i = 0; i < begin; i++) {
}
for (NSInteger i = begin; i <= end; i++) {
}
//打印堆排序
NSLog(@"堆排序升序结果是--->%@",array);
}
4. 插入排序(insert sort)
4.1 网上文字理解
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
4.2 算法步骤
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
4.3 动图演示
4.4 插入排序代码示例
- (void)insertSortWithArray:(NSMutableArray *)array {
NSInteger j;
for (NSInteger i = 1; i < array.count; i++) {
//取出每一个待插入的数据,从array[1]开始查找
NSInteger temp = [array[i] integerValue];
for (j = i - 1; j >= 0 && temp < [array[j] integerValue]; j--) {
//如果之前的数比temp大,就将这个数往后移动一个位置,留出空来让temp插入,和整理扑克牌类似
[array[j + 1] integerValue] = [array[j] integerValue]];
array[j] = [NSNumber numberWithInteger:temp];
}
}
}
归并排序、希尔排序、基数排序、桶排序、计数排序几个不常用的算法,算是高级算法吧,具体是不是菜鸟本人觉得还是只看得懂文字,具体文字理论和实现原理还有待进一步学习,网上搜罗了很多资料说,一般需要掌握几种常用的算法冒泡、快速、插入、选择就够了,我觉得技术还是多多益善,当然前提是完全掌握了,能够手写,并且能够说出道理是最好的,不然都是临时记忆,好吧,我目前理解的也是不够深透,还是网上记录一些笔记,自己也好临摹学习下,方便日后能消化这些计算机常用的算法
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:519832104 不管你是小白还是大牛欢迎入驻,分享经验,讨论技术,大家一起交流学习成长!
另附上一份各好友收集的大厂面试题,需要iOS开发学习资料、面试真题,可以添加iOS开发进阶交流群,进群可自行下载!
5. 归并排序(merge sort)
5.1 网上文字理解
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
5.2 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
5.3 动图演示
5.4 归并排序代码示例
//自顶向下的归并排序
/**
递归使用归并排序,对array[left...right]的范围进行排序
@param array 数组
@param left 左边界
@param right 右边界
*/
- (void)mergeSortWithArray:(NSMutableArray *)array
left:(NSInteger)left
right:(NSInteger)right {
//判断递归到底的情况
if (left >= right) {
//这时候只有一个元素或者是不存在的情况
return;
}
//中间索引的位置
NSInteger middle = (right + left) / 2;
//对 left --- middle 区间的元素进行排序操作
[self mergeSortWithArray:array left:left right:middle];
//对 middle + 1 ---- right 区间的元素进行排序操作
[self mergeSortWithArray:array left:middle + 1 right:right];
//两边排序完成后进行归并操作
[self mergeSortWithArray:array left:left middle:middle right:right];
}
/**
对 [left middle] 和 [middle + 1 right]这两个区间归并操作
@param array 传入的数组
@param left 左边界
@param middle 中间位置
@param right 右边界
*/
- (void)mergeSortWithArray:(NSMutableArray *)array
left:(NSInteger)left
middle:(NSInteger)middle
right:(NSInteger)right {
//拷贝一个数组出来
NSMutableArray *copyArray = [NSMutableArray arrayWithCapacity:right - left + 1];
for (NSInteger i = left; i <= right; i++) {
//这里要注意有left的偏移量,所以copyArray赋值的时候要减去left
copyArray[i - left] = array[i];
}
NSInteger i = left, j = middle + 1;
//循环从left开始到right区间内给数组重新赋值,注意赋值的时候也是从left开始的,不要习惯写成了从0开始,还有都是闭区间
for (NSInteger k = left; k <= right; k++) {
//当左边界超过中间点时 说明左半部分数组越界了 直接取右边部分的数组的第一个元素即可
if (i > middle) {
//给数组赋值 注意偏移量left 因为这里是从left开始的
array[k] = copyArray[j - left];
//索引++
j++;
} else if (j > right) {//当j大于右边的边界时证明有半部分数组越界了,直接取左半部分的第一个元素即可
array[k] = copyArray[i - left];
//索引++
i++;
} else if (copyArray[i - left] > copyArray[j - left]) {//左右两半部分数组比较
//当右半部分数组的第一个元素要小时 给数组赋值为右半部分的第一个元素
array[k] = copyArray[j - left];
//右半部分索引加1
j++;
} else {//右半部分数组首元素大于左半部分数组首元素
array[k] = copyArray[i - left];
i++;
}
}
}
6. 希尔排序(shell sort)
6.1 网上文字理解
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
6.2 算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
6.4 希尔排序代码示例
- (void)shellAscendingOrderSort:(NSMutableArray *)ascendingArr {
NSMutableArray *buckt = [self createBucket];
NSNumber *maxnumber = [self listMaxItem:ascendingArr];
NSInteger maxLength = numberLength(maxnumber);
for (int digit = 1; digit <= maxLength; digit++) {
// 入桶
for (NSNumber *item in ascendingArr) {
NSInteger baseNumber = [self fetchBaseNumber:item digit:digit];
NSMutableArray *mutArray = buckt[baseNumber];
[mutArray addObject:item];
}
NSInteger index = 0;
for (int i = 0; i < buckt.count; i++) {
NSMutableArray *array = buckt[i];
while (array.count != 0) {
NSNumber *number = [array objectAtIndex:0];
ascendingArr[index] = number;
[array removeObjectAtIndex:0];
index++;
}
}
}
NSLog(@"希尔升序排序结果:%@", ascendingArr);
}
- (NSMutableArray *)createBucket {
NSMutableArray *bucket = [NSMutableArray array];
for (int index = 0; index < 10; index++) {
NSMutableArray *array = [NSMutableArray array];
[bucket addObject:array];
}
return bucket;
}
- (NSNumber *)listMaxItem:(NSArray *)list {
NSNumber *maxNumber = list[0];
for (NSNumber *number in list) {
if ([maxNumber integerValue] < [number integerValue]) {
maxNumber = number;
}
}
return maxNumber;
}
NSInteger numberLength(NSNumber *number) {
NSString *string = [NSString stringWithFormat:@"%ld", (long)[number integerValue]];
return string.length;
}
- (NSInteger)fetchBaseNumber:(NSNumber *)number digit:(NSInteger)digit {
if (digit > 0 && digit <= numberLength(number)) {
NSMutableArray *numbersArray = [NSMutableArray array];
NSString *string = [NSString stringWithFormat:@"%ld", [number integerValue]];
for (int index = 0; index < numberLength(number); index++) {
[numbersArray addObject:[string substringWithRange:NSMakeRange(index, 1)]];
}
NSString *str = numbersArray[numbersArray.count - digit];
return [str integerValue];
}
return 0;
}
7. 基数排序(radix sort)
7.1 文字理解
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
7.2 基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
7.3 动图演示
7.4 基数排序代码示例
- (void)radixAscendingOrderSort:(NSMutableArray *)ascendingArr {
NSMutableArray *buckt = [self createBucket];
NSNumber *maxnumber = [self listMaxItem:ascendingArr];
NSInteger maxLength = numberLength(maxnumber);
for (int digit = 1; digit <= maxLength; digit++) {
// 入桶
for (NSNumber *item in ascendingArr) {
NSInteger baseNumber = [self fetchBaseNumber:item digit:digit];
NSMutableArray *mutArray = buckt[baseNumber];
[mutArray addObject:item];
}
NSInteger index = 0;
for (int i = 0; i < buckt.count; i++) {
NSMutableArray *array = buckt[i];
while (array.count != 0) {
NSNumber *number = [array objectAtIndex:0];
ascendingArr[index] = number;
[array removeObjectAtIndex:0];
index++;
}
}
}
NSLog(@"基数升序排序结果:%@", ascendingArr);
}
8. 计数排序(counting sort)
8.1 文字理解
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.2 动图演示
9. 桶排序(bucket sort)
9.1 文字理解
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
9.2 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
9.3 什么时候最慢
当输入的数据被分配到了同一个桶中。