關於演算法的想法 由於面試可能需要手寫演算法,網上搜羅了一些資料,整理了下演算法的OC的實現代碼,雖然平時開發中一般用不到,但是多積累一些技術知識,還是對以後發展大有裨益的 github上搜集的幾大演算法原理和實現代碼,只有JavaScript、Python、Go、Java的實現代碼 演算法文字理解和OC代碼 ...
關於演算法的想法
由於面試可能需要手寫演算法,網上搜羅了一些資料,整理了下演算法的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 什麼時候最慢
當輸入的數據被分配到了同一個桶中。