無標(biāo)題文章

http://blog.csdn.net/david21984/article/details/57451917

iOS 面試題(一)尋找最近公共 View

題目:找出兩個(gè) UIView 的最近的公共 View,如果不存在,則輸出 nil 。




分析:這其實(shí)是數(shù)據(jù)結(jié)構(gòu)里面的找最近公共祖先的問題。

一個(gè)UIViewController中的所有view之間的關(guān)系其實(shí)可以看成一顆樹,UIViewController的view變量是這顆樹的根節(jié)點(diǎn),其它的view都是根節(jié)點(diǎn)的直接或間接子節(jié)點(diǎn)。

所以我們可以通過 view 的 superview 屬性,一直找到根節(jié)點(diǎn)。需要注意的是,在代碼中,我們還需要考慮各種非法輸入,如果輸入了 nil,則也需要處理,避免異常。以下是找到指定 view 到根 view 的路徑代碼:

+ (NSArray*)superViews:(UIView*)view {if(view ==nil) {return@[];? ? }NSMutableArray*result = [NSMutableArrayarray];while(view !=nil) {? ? ? ? [result addObject:view];? ? ? ? view = view.superview;? ? }return[resultcopy];}

然后對(duì)于兩個(gè) view A 和 view B,我們可以得到兩個(gè)路徑,而本題中我們要找的是這里面最近的一個(gè)公共節(jié)點(diǎn)。

一個(gè)簡(jiǎn)單直接的辦法:拿第一個(gè)路徑中的所有節(jié)點(diǎn),去第二個(gè)節(jié)點(diǎn)中查找。假設(shè)路徑的平均長(zhǎng)度是 N,因?yàn)槊總€(gè)節(jié)點(diǎn)都要找 N 次,一共有 N 個(gè)節(jié)點(diǎn),所以這個(gè)辦法的時(shí)間復(fù)雜度是 O(N^2)。

+ (UIView*)commonView_1:(UIView*)viewA andView:(UIView*)viewB {NSArray*arr1 = [selfsuperViews:viewA];NSArray*arr2 = [selfsuperViews:viewB];for(NSUIntegeri =0; i < arr1.count; ++i) {UIView*targetView = arr1[i];for(NSUIntegerj =0; j < arr2.count; ++j) {if(targetView == arr2[j]) {returntargetView;? ? ? ? ? ? }? ? ? ? }? ? }returnnil;}

一個(gè)改進(jìn)的辦法:我們將一個(gè)路徑中的所有點(diǎn)先放進(jìn) NSSet 中。因?yàn)?NSSet 的內(nèi)部實(shí)現(xiàn)是一個(gè) hash 表,所以查找元素的時(shí)間復(fù)雜度變成了 O(1),我們一共有 N 個(gè)節(jié)點(diǎn),所以總時(shí)間復(fù)雜度優(yōu)化到了 O(N)。

+ (UIView*)commonView_2:(UIView*)viewA andView:(UIView*)viewB {NSArray*arr1 = [selfsuperViews:viewA];NSArray*arr2 = [selfsuperViews:viewB];NSSet*set = [NSSetsetWithArray:arr2];for(NSUIntegeri =0; i < arr1.count; ++i) {UIView*targetView = arr1[i];if([set containsObject:targetView]) {returntargetView;? ? ? ? }? ? }returnnil;}

除了使用 NSSet 外,我們還可以使用類似歸并排序的思想,用兩個(gè)「指針」,分別指向兩個(gè)路徑的根節(jié)點(diǎn),然后從根節(jié)點(diǎn)開始,找第一個(gè)不同的節(jié)點(diǎn),第一個(gè)不同節(jié)點(diǎn)的上一個(gè)公共節(jié)點(diǎn),就是我們的答案。代碼如下:

/* O(N) Solution */+ (UIView *)commonView_3:(UIView *)viewAandView:(UIView*)viewB {? ? NSArray *arr1 = [selfsuperViews:viewA];NSArray *arr2 = [selfsuperViews:viewB];NSIntegerp1= arr1.count -1;NSIntegerp2= arr2.count -1;UIView *answer = nil;while(p1>=0&&p2>=0) {if(arr1[p1] == arr2[p2]) {? ? ? ? ? ? answer = arr1[p1];}p1--;p2--;}? ? return answer;}

我們還可以使用 UIView 的 isDescendant 方法來簡(jiǎn)化我們的代碼,不過這樣寫的話,時(shí)間復(fù)雜度應(yīng)該也是 O(N^2) 的。lexrus 提供了如下的 Swift 版本的代碼:

/// without flatMapextensionUIView{funccommonSuperview(of view: UIView)->UIView? {iflets = superview {ifview.isDescendant(of: s) {returns? ? ? ? ? ? }else{returns.commonSuperview(of: view)? ? ? ? ? ? }? ? ? ? }returnnil}}

特別地,如果我們利用 Optinal 的 flatMap 方法,可以將上面的代碼簡(jiǎn)化得更短,基本上算是一行代碼搞定。怎么樣,你學(xué)會(huì)了嗎?

extension UIView {funccommonSuperview(of view: UIView)->UIView? {returnsuperview.flatMap {? ? ? ? ? ? view.isDescendant(of: $0) ?? ? ? ? ? ? ? $0 : $0.commonSuperview(of: view)? ? ? ? }? ? }}

iOS 面試題(二)什么時(shí)候在 block 中不需要使用 weakSelf

問題:我們知道,在使用 block 的時(shí)候,為了避免產(chǎn)生循環(huán)引用,通常需要使用 weakSelf 與 strongSelf,寫下面這樣的代碼:

__weaktypeof(self) weakSelf =self;[selfdoSomeBlockJob:^{? ? __strongtypeof(weakSelf) strongSelf = weakSelf;if(strongSelf) {? ? ? ? ...? ? }}];

那么請(qǐng)問:什么時(shí)候在 block里面用self,不需要使用weakself?

當(dāng)block本身不被self 持有,而被別的對(duì)象持有,同時(shí)不產(chǎn)生循環(huán)引用的時(shí)候,就不需要使用weakself了。最常見的代碼就是UIView的動(dòng)畫代碼,我們?cè)谑褂肬IView animateWithDuration:animations方法 做動(dòng)畫的時(shí)候,并不需要使用weakself,因?yàn)橐贸钟嘘P(guān)系是:

UIView 的某個(gè)負(fù)責(zé)動(dòng)畫的對(duì)象持有block,block 持有了self因?yàn)?self 并不持有 block,所以就沒有循環(huán)引用產(chǎn)生,因?yàn)榫筒恍枰褂?weak self 了。

[UIView animateWithDuration:0.2 animations:^{

self.alpha = 1;

}];

當(dāng)動(dòng)畫結(jié)束時(shí),UIView會(huì)結(jié)束持有這個(gè) block,如果沒有別的對(duì)象持有block的話,block 對(duì)象就會(huì)釋放掉,從而 block會(huì)釋放掉對(duì)于 self 的持有。整個(gè)內(nèi)存引用關(guān)系被解除。

iOS 面試題(三)什么時(shí)候在 block 中不需要使用 weakSelf

我們知道,在使用 block 的時(shí)候,為了避免產(chǎn)生循環(huán)引用,通常需要使用 weakSelf 與 strongSelf,寫下面這樣的代碼:

__weaktypeof(self) weakSelf =self;[selfdoSomeBackgroundJob:^{? ? __strongtypeof(weakSelf) strongSelf = weakSelf;if(strongSelf) {? ? ? ? ...? ? }}];

那么請(qǐng)問:為什么 block 里面還需要寫一個(gè) strong self,如果不寫會(huì)怎么樣?

在 block 中先寫一個(gè) strong self,其實(shí)是為了避免在 block 的執(zhí)行過程中,突然出現(xiàn) self 被釋放的尷尬情況。通常情況下,如果不這么做的話,還是很容易出現(xiàn)一些奇怪的邏輯,甚至閃退。

我們以AFNetworking中的AFNetworkReachabilityManager.m的一段代碼舉例:

__weak__typeof(self)weakSelf=self;AFNetworkReachabilityStatusBlockcallback = ^(AFNetworkReachabilityStatus status) {? ? __strong __typeof(weakSelf)strongSelf= weakSelf;strongSelf.networkReachabilityStatus= status;if(strongSelf.networkReachabilityStatusBlock){strongSelf.networkReachabilityStatusBlock(status);}};

如果沒有strongSelf的那行代碼,那么后面的每一行代碼執(zhí)行時(shí),self都可能被釋放掉了,這樣很可能造成邏輯異常。

特別是當(dāng)我們正在執(zhí)行 strongSelf.networkReachabilityStatusBlock(status); 這個(gè) block閉包時(shí),如果這個(gè) block 執(zhí)行到一半時(shí) self 釋放,那么多半情況下會(huì) Crash。

這里有一篇文章詳細(xì)解釋了這個(gè)問題:點(diǎn)擊查看文章

昨天的讀者中,拓荒者 和 陳祥龍 同學(xué)在評(píng)論中也正確回答出了本題。

拓荒者:

1.在block里使用strongSelf是防止在block執(zhí)行過程中self被釋放。 2.可以通過在執(zhí)行完block代碼后手動(dòng)把block置為nil來打破引用循環(huán),AFNetworking就是這樣處理的,避免使用者不了解引用循環(huán)造成內(nèi)存泄露。實(shí)際業(yè)務(wù)中暫時(shí)沒遇到這種需求,請(qǐng)巧哥指點(diǎn)什么情況下會(huì)有這種需求。

陳祥龍:

strongSelf 一般是在為了避免 block 回調(diào)時(shí) weak Self變成了nil ,異步執(zhí)行一些操作時(shí)可能會(huì)出現(xiàn)這種情況,不知道我說得對(duì)不對(duì)。因業(yè)務(wù)需要不能使用weakSelf 這種情況還真沒遇到過

另外,還有讀者提了兩個(gè)有意思的問題,大家可以思考一下:

Yuen 提問:“數(shù)組” 和 “字典” 的 enumeratXXXUsingBlock: 是否要使用 weakSelf 和 strongSelf 呢?

瀟湘雨同學(xué)提問:block 里 strong self 后,block 不是也會(huì)持有 self 嗎?而 self 又持有 block ,那不是又循環(huán)引用了?

iOS 面試題(四):block 什么時(shí)候需要構(gòu)造循環(huán)引用

問題:有沒有這樣一個(gè)需求場(chǎng)景,block 會(huì)產(chǎn)生循環(huán)引用,但是業(yè)務(wù)又需要你不能使用 weak self? 如果有,請(qǐng)舉一個(gè)例子并且解釋這種情況下如何解決循環(huán)引用問題。

答案:需要不使用 weak self 的場(chǎng)景是:你需要構(gòu)造一個(gè)循環(huán)引用,以便保證引用雙方都存在。比如你有一個(gè)后臺(tái)的任務(wù),希望任務(wù)執(zhí)行完后,通知另外一個(gè)實(shí)例。在我們開源的 YTKNetwork 網(wǎng)絡(luò)庫(kù)的源碼中,就有這樣的場(chǎng)景。

在 YTKNetwork 庫(kù)中,我們的每一個(gè)網(wǎng)絡(luò)請(qǐng)求 API 會(huì)持有回調(diào)的 block,回調(diào)的 block 會(huì)持有 self,而如果 self 也持有網(wǎng)絡(luò)請(qǐng)求 API 的話,我們就構(gòu)造了一個(gè)循環(huán)引用。雖然我們構(gòu)造出了循環(huán)引用,但是因?yàn)樵诰W(wǎng)絡(luò)請(qǐng)求結(jié)束時(shí),網(wǎng)絡(luò)請(qǐng)求 API 會(huì)主動(dòng)釋放對(duì) block 的持有,因此,整個(gè)循環(huán)鏈條被解開,循環(huán)引用就被打破了,所以不會(huì)有內(nèi)存泄漏問題。代碼其實(shí)很簡(jiǎn)單,如下所示:

//? YTKBaseRequest.m- (void)clearCompletionBlock {// nil out to break the retain cycle.self.successCompletionBlock =nil;self.failureCompletionBlock =nil;}

總結(jié)來說,解決循環(huán)引用問題主要有兩個(gè)辦法:

第一個(gè)辦法是「事前避免」,我們?cè)跁?huì)產(chǎn)生循環(huán)引用的地方使用 weak 弱引用,以避免產(chǎn)生循環(huán)引用。

第二個(gè)辦法是「事后補(bǔ)救」,我們明確知道會(huì)存在循環(huán)引用,但是我們?cè)诤侠淼奈恢弥鲃?dòng)斷開環(huán)中的一個(gè)引用,使得對(duì)象得以回收。

iOS 面試題(五):weak 的內(nèi)部實(shí)現(xiàn)原理

問題:weak 變量在引用計(jì)數(shù)為0時(shí),會(huì)被自動(dòng)設(shè)置成 nil,這個(gè)特性是如何實(shí)現(xiàn)的?

答案:在Friday QA上,有一期專門介紹 weak的實(shí)現(xiàn)原理。

《Objective-C高級(jí)編程》一書中也介紹了相關(guān)的內(nèi)容。

簡(jiǎn)單來說,系統(tǒng)有一個(gè)全局的 CFMutableDictionary 實(shí)例,來保存每個(gè)對(duì)象的 weak 指針列表,因?yàn)槊總€(gè)對(duì)象可能有多個(gè) weak 指針,所以這個(gè)實(shí)例的值是 CFMutableSet 類型。

剩下我們要做的,就是在引用計(jì)數(shù)變成 0 的時(shí)候,去這個(gè)全局的字典里面,找到所有的 weak 指針,將其值設(shè)置成 nil。如何做到這一點(diǎn)呢?Friday QA上介紹了一種類似 KVO 實(shí)現(xiàn)的方式。當(dāng)對(duì)象存在 weak 指針時(shí),我們可以將這個(gè)實(shí)例指向一個(gè)新創(chuàng)建的子類,然后修改這個(gè)子類的 release 方法,在 release 方法中,去從全局的 CFMutableDictionary 字典中找到所有的 weak 對(duì)象,并且設(shè)置成 nil。我摘抄了 Friday QA 上的實(shí)現(xiàn)的核心代碼,如下:

Classsubclass = objc_allocateClassPair(class, newNameC,0);Methodrelease=class_getInstanceMethod(class, @selector(release));Methoddealloc=class_getInstanceMethod(class, @selector(dealloc));class_addMethod(subclass, @selector(release), (IMP)CustomSubclassRelease, method_getTypeEncoding(release));class_addMethod(subclass, @selector(dealloc), (IMP)CustomSubclassDealloc, method_getTypeEncoding(dealloc));objc_registerClassPair(subclass);

當(dāng)然,這并不代表蘋果官方是這么實(shí)現(xiàn)的,因?yàn)樘O果的這部分代碼并沒有開源。《Objective-C高級(jí)編程》一書中介紹了 GNUStep 項(xiàng)目中的開源代碼,思想也是類似的。所以我認(rèn)為雖然實(shí)現(xiàn)細(xì)節(jié)會(huì)有差異,但是大致的實(shí)現(xiàn)思路應(yīng)該差別不大。

iOS 面試題(六):自己寫的 view 成員,應(yīng)該用 weak 還是 strong?

問題:我們知道,從 Storyboard 往編譯器拖出來的 UI 控件的屬性是 weak 的,如下所示

@property(weak,nonatomic)IBOutletUIButton*myButton;

那么,如果有一些 UI 控件我們要用代碼的方式來創(chuàng)建,那么它應(yīng)該用 weak 還是 strong 呢?為什么?

答案:這是一道有意思的問題,這個(gè)問題是我當(dāng)時(shí)和 Lancy 一起寫猿題庫(kù) App 時(shí)產(chǎn)生的一次小爭(zhēng)論。簡(jiǎn)單來說,這道題并沒有標(biāo)準(zhǔn)答案,但是答案背后的解釋卻非常有價(jià)值,能夠看出一個(gè)人對(duì)于引用計(jì)數(shù),對(duì)于 view 的生命周期的理解是否到位。

從昨天的評(píng)論上,我們就能看到一些理解非常不到位的解釋,例如:

@spume 說:Storyboard 拖線使用 weak 是為了規(guī)避出現(xiàn)循環(huán)引用的問題。

這個(gè)理解是錯(cuò)誤的,Storyboard 拖出來的控件即使是 strong 的,也不會(huì)有循環(huán)引用問題。

我認(rèn)為 UI 控件用默認(rèn)用 weak,根源還是蘋果希望只有這些 UI 控件的父 View 來強(qiáng)引用它們,而 ViewController 只需要強(qiáng)引用 ViewController.view 成員,則可以間接持有所有的 UI 控件。這樣有一個(gè)好處是:在以前,當(dāng)系統(tǒng)收到 Memory Warning 時(shí),會(huì)觸發(fā) ViewController 的 viewDidUnload 方法,這樣的弱引用方式,可以讓整個(gè) view 整體都得到釋放,也更方便重建時(shí)整體重新構(gòu)造。

但是首先 viewDidUnload 方法在 iOS 6 開始就被廢棄掉了,蘋果用了更簡(jiǎn)單有效地方式來解決內(nèi)存警告時(shí)的視圖資源釋放,具體如何做的呢?嗯,這個(gè)可以當(dāng)作某一期的面試題展開介紹。總之就是,除非你特殊地操作 view 成員,ViewController.view 的生命期和 ViewController 是一樣的了。

所以在這種情況下,其實(shí) UI 控件是不是 weak 其實(shí)關(guān)系并不大。當(dāng) UI 控件是 weak 時(shí),它的引用計(jì)數(shù)是 1,持有它的是它的 superview,當(dāng) UI 控件是 strong 時(shí),它的引用計(jì)數(shù)是 2,持有它的有兩個(gè)地方,一個(gè)是它的 superview,另一個(gè)是這個(gè) strong 的指針。UI 控件并不會(huì)持有別的對(duì)象,所以,不管是手寫代碼還是 Storyboard,UI 控件是 strong 都不會(huì)有循環(huán)引用的。

那么回到我們的最初的問題,自己寫的 view 成員,應(yīng)該用 weak 還是 strong?我個(gè)人覺得應(yīng)該用 strong,因?yàn)橛?weak 并沒有什么特別的優(yōu)勢(shì),加上上一篇面試題文章中,我們還看到,其實(shí) weak 變量會(huì)有額外的系統(tǒng)維護(hù)開銷的,如果你沒有使用它的特別的理由,那么用 strong 的話應(yīng)該更好。

另外有讀者也提到,如果你要做 Lazy 加載,那么你也只能選擇用 strong。

當(dāng)然,如果你非要用 weak,其實(shí)也沒什么問題,只需要注意在賦值前,先把這個(gè)對(duì)象用 addSubView 加到父 view 上,否則可能剛剛創(chuàng)建完,它就被釋放了。

在我心目中,這才是我喜歡的面試題,沒有標(biāo)準(zhǔn)答案,每種方案各有各的特點(diǎn),面試者能夠足夠分清楚每種方案的優(yōu)缺點(diǎn),結(jié)合具體的場(chǎng)景做選擇,這才是優(yōu)秀的面試者。

1.懶加載的對(duì)象必須用strong的原因在于,如果使用weak,對(duì)象沒有被沒有被強(qiáng)引用,過了懶加載對(duì)象就會(huì)被釋放掉。

iOS 面試題(七):為什么 Objective-C 的方法調(diào)用要用方括號(hào)?

問題:為什么 Objective-C 的方法調(diào)用要用方括號(hào) [obj foo],而不是別的語(yǔ)言常常使用的點(diǎn) obj.foo ?

答案:

首先要說的是,Objective-C 的歷史相當(dāng)久遠(yuǎn),如果你查 wiki 的話,你會(huì)發(fā)現(xiàn):Objective-C 和 C++ 這兩種語(yǔ)言的發(fā)行年份都是 1983 年。在設(shè)計(jì)之初,二者都是作為 C 語(yǔ)言的面向?qū)ο蟮慕影嗳耍M蔀槭聦?shí)上的標(biāo)準(zhǔn)。最后結(jié)果大家都知道了,C++ 最終勝利了,而 Objective-C 在之后的幾十年中,基本上變成了蘋果自己家玩的玩具。不過最終,由于 iPhone 的出現(xiàn),Objective-C 迎來了第二春,在 TOBIE 語(yǔ)言排行榜上,從 20 名開外一路上升,排名曾經(jīng)超越過 C++,達(dá)到了第三名(下圖),但是隨著 Swift 的出現(xiàn),Objective-C 的排名則一路下滑。

TOBIE排行版

Objective-C 在設(shè)計(jì)之初參考了不少 Smalltalk 的設(shè)計(jì),而消息發(fā)送則是向 Smalltalk 學(xué)來的。Objective-C 當(dāng)時(shí)采用了方括號(hào)的形式來表示發(fā)送消息,為什么沒有選擇用點(diǎn)呢?我個(gè)人覺得是,當(dāng)時(shí)市面上并沒有別的面向?qū)ο笳Z(yǔ)言的設(shè)計(jì)參考,而 Objective-C 「發(fā)明」了方括號(hào)的形式來給對(duì)象發(fā)消息,而 C++ 則「發(fā)明」了用點(diǎn)的方式來 “發(fā)消息”。有人可能會(huì)爭(zhēng)論說 C++ 的「點(diǎn)」并不是真正的發(fā)消息,但是其實(shí)二者都是表示「調(diào)用對(duì)象所屬的成員函數(shù)」。

另外,有讀者評(píng)論說使用方括號(hào)的形式是為了向下兼容 C 語(yǔ)言,我并不覺得中括號(hào)是唯一選擇,C++ 不也兼容了 C 語(yǔ)言么?Swift 不也可以調(diào)用 C 函數(shù)么?

最終,其實(shí)是 C++ 的「發(fā)明」顯得更舒服一些,所以后來的各種語(yǔ)言都借鑒了 C++ 的這種設(shè)計(jì),也包括 Objective-C 在內(nèi)。Objective-C 2.0 版本中,引入了 dot syntax,即:

a = obj.foo 等價(jià)于 a = [obj foo]

obj.foo = 1 則等價(jià)于 [obj setFoo:1]

Objective-C 其實(shí)在設(shè)計(jì)之中確實(shí)是比較特立獨(dú)行的,除了方括號(hào)的函數(shù)調(diào)用方式外,還包括比較長(zhǎng)的,可讀性很強(qiáng)的函數(shù)命名風(fēng)格。

我個(gè)人并不討厭 Objective-C 的這種設(shè)計(jì),但是從 Swift 語(yǔ)言的設(shè)計(jì)來看,蘋果也開始放棄一些 Objective-C 的特點(diǎn)了,比如就去掉了方括號(hào)這種函數(shù)調(diào)用方式。

所以,回到我們的問題,我個(gè)人認(rèn)為,答案就是:Objective-C 在 1983 年設(shè)計(jì)的時(shí)候,并沒有什么有效的效仿對(duì)象,于是就發(fā)明了一種有特點(diǎn)的函數(shù)調(diào)用方式,現(xiàn)在看起來,這種方式比點(diǎn)操作符還是略遜一籌。

大多數(shù)語(yǔ)言一旦被設(shè)計(jì)好,就很難被再次修改,應(yīng)該說 Objective-C 發(fā)明在 30 年前,還是非常優(yōu)秀的,它的面向?qū)ο蠡O(shè)計(jì)得非常純粹,比 C++ 要全面得多,也比 C++ 要簡(jiǎn)單得多。

iOS 面試題(八):實(shí)現(xiàn)一個(gè)嵌套數(shù)組的迭代器

問題:

給你一個(gè)嵌套的 NSArray 數(shù)據(jù),實(shí)現(xiàn)一個(gè)迭代器類,該類提供一個(gè) next() 方法,可以依次的取出這個(gè) NSArray 中的數(shù)據(jù)。

比如 NSArray 如果是 [1,[4,3],6,[5,[1,0]]], 則最終應(yīng)該輸出:1, 4, 3, 6, 5, 1, 0 。

另外,實(shí)現(xiàn)一個(gè) allObjects 方法,可以一次性取出所有元素。

給你一個(gè)嵌套的 NSArray 數(shù)據(jù),實(shí)現(xiàn)一個(gè)迭代器類,該類提供一個(gè) next() 方法,可以依次的取出這個(gè) NSArray 中的數(shù)據(jù)。

解答:

本題的代碼稍長(zhǎng),完整的代碼我放在git上了,以下是講解。

先說第二問吧,第二問比較簡(jiǎn)單:實(shí)現(xiàn)一個(gè) allObjects 方法,可以一次性取出所有元素。

對(duì)于此問,我們可以實(shí)現(xiàn)一個(gè)遞歸函數(shù),在函數(shù)中判斷數(shù)組中的元素是否又是數(shù)組,如果是的話,就遞歸調(diào)用自己,如果不是數(shù)組,則加入到一個(gè) NSMutableArray 中即可。下面是示例代碼:

- (NSArray*)allObjects {NSMutableArray*result= [NSMutableArrayarray];? ? [self fillArray:_originArray into:result];returnresult;}- (void)fillArray:(NSArray*)arrayinto:(NSMutableArray*)result{for(NSUIntegeri =0; i

如果你還在糾結(jié)掌握遞歸有什么意義的話,歡迎翻翻我半年前寫的另一篇文章:遞歸的故事(上)遞歸的故事(下)

接下來讓我們來看第一問,在同學(xué)的回復(fù)中,我看到很多人用第二問的辦法,把數(shù)組整個(gè)另外保存一份,然后再記錄一個(gè)下標(biāo),每次返回其中一個(gè)。這個(gè)方法當(dāng)然是可行的,但是大部分的迭代器通常都不會(huì)這么實(shí)現(xiàn)。因?yàn)檫@么實(shí)現(xiàn)的話,數(shù)組需要整個(gè)復(fù)制一遍,空間復(fù)雜度是 O(N)。

所以,我個(gè)人認(rèn)為本題第一問更好的解法是:

記錄下遍歷的位置,然后每次遍歷時(shí)更新位置。由于本題中元素是一個(gè)嵌套數(shù)組,所以我們?yōu)榱擞涗浵挛恢茫托枰獌蓚€(gè)變量:一個(gè)是當(dāng)前正在遍歷的子數(shù)組,另一個(gè)是這個(gè)數(shù)組遍歷到的位置。

我在實(shí)現(xiàn)的時(shí)候,定義了一個(gè)名為 NSArrayIteratorCursor 的類來記錄這些內(nèi)容,NSArrayIteratorCursor 的定義和實(shí)現(xiàn)如下:

@interfaceNSArrayIteratorCursor:NSObject@property(nonatomic)NSArray*array;@property(nonatomic)NSUIntegerindex;@end@implementationNSArrayIteratorCursor- (id)initWithArray:(NSArray*)array {self= [superinit];if(self) {? ? ? ? _array = array;? ? ? ? _index =0;? ? }returnself;}@end

由于數(shù)組在遍歷的時(shí)候可能產(chǎn)生遞歸,就像我們實(shí)現(xiàn) allObjects 方法那樣。所以我們需要處理遞歸時(shí)的 NSArrayIteratorCursor 的保存,我在實(shí)現(xiàn)的時(shí)候,拿數(shù)組當(dāng)作棧,來實(shí)現(xiàn)保存遍歷時(shí)的狀態(tài)。

最終,我實(shí)現(xiàn)了一個(gè)迭代器類,名字叫 NSArrayIterator,用于最終提供 next 方法的實(shí)現(xiàn)。這個(gè)類有兩個(gè)私有變量,一個(gè)是剛剛說的那個(gè)棧,另一個(gè)是原數(shù)組的引用。

@interfaceNSArrayIterator:NSObject- (id)initWithArray:(NSArray*)array;- (id)next;- (NSArray*)allObjects;@end@implementationNSArrayIterator{NSMutableArray*_stack;NSArray*_originArray;}

在初使化的時(shí)候,我們初始化遍歷位置的代碼如下:

- (id)initWithArray:(NSArray*)array {self= [superinit];if(self) {? ? ? ? _originArray = array;? ? ? ? _stack = [NSMutableArrayarray];? ? ? ? [selfsetupStack];? ? }returnself;}- (void)setupStack {NSArrayIteratorCursor*c = [[NSArrayIteratorCursoralloc] initWithArray:_originArray];? ? [_stack addObject:c];}

接下來就是最關(guān)鍵的代碼了,即實(shí)現(xiàn) next 方法,在 next 方法的實(shí)現(xiàn)邏輯中,我們需要:

判斷棧是否為空,如果為空則返回 nil。

從棧中取出元素,看是否遍歷到了結(jié)尾,如果是的話,則出棧。

判斷第 2 步是否使棧為空,如果為空,則返回 nil。

終于拿到元素了,這一步判斷拿到的元素是否是數(shù)組。

如果是數(shù)組,則重新生成一個(gè)遍歷的 NSArrayIteratorCursor 對(duì)象,放到棧中。

重新從棧中拿出第一個(gè)元素,循環(huán)回到第 4 步的判斷。

如果到了這一步,說明拿到了一個(gè)非數(shù)組的元素,這樣就可以把元素返回,同時(shí)更新索引到下一個(gè)位置。

以下是相關(guān)的代碼,對(duì)于沒有算法基礎(chǔ)的同學(xué),可能讀起來還是比較累,其實(shí)我寫起來也不快,所以希望你能多理解一下,其實(shí)核心思想就是手工操作棧的入棧和出棧:

- (id)next {//? 1. 判斷棧是否為空,如果為空則返回 nil。if([_stackcount] ==0) {returnnil;? ? }// 2. 從棧中取出元素,看是否遍歷到了結(jié)尾,如果是的話,則出棧。NSArrayIteratorCursor*c;c= [_stack lastObject];while(c.index ==c.array.count&& _stack.count>0) {? ? ? ? [_stack removeLastObject];c= [_stack lastObject];? ? }// 3. 判斷第 2 步是否使棧為空,如果為空,則返回 nil。if(_stack.count==0) {returnnil;? ? }// 4. 終于拿到元素了,這一步判斷拿到的元素是否是數(shù)組。id item =c.array[c.index];while([item isKindOfClass:[NSArrayclass]]){c.index++;// 5. 如果是數(shù)組,則重新生成一個(gè)遍歷的 NSArrayIteratorCursor 對(duì)象,放到棧中。NSArrayIteratorCursor*nc = [[NSArrayIteratorCursoralloc] initWithArray:item];? ? ? ? [_stack addObject:nc];// 6. 重新從棧中拿出第一個(gè)元素,循環(huán)回到第 4 步的判斷。c= nc;? ? ? ? item =c.array[c.index];? ? }// 7. 如果到了這一步,說明拿到了一個(gè)非數(shù)組的元素,這樣就可以把元素返回,同時(shí)更新索引到下一個(gè)位置。c.index++;returnitem;}

在讀者回復(fù)中,聽榆大叔 和 yiplee 同學(xué)用了類似的做法,他們的代碼在:

聽榆大叔yiplee

最終,我想說這個(gè)只是我個(gè)人想出來的解法,很可能不是最優(yōu)的,甚至可能也有很多問題,比如,這個(gè)代碼有很多可以進(jìn)一步 challenge 的地方:

這個(gè)代碼是線程安全的嗎?如果我們要實(shí)現(xiàn)一個(gè)線程安全的迭代器,應(yīng)該怎么做?

如果在使用迭代器的時(shí)候,數(shù)組被修改了,會(huì)怎么樣?

如何檢測(cè)在遍歷元素的時(shí)候,數(shù)組被修改了?

如何避免在遍歷元素的時(shí)候,數(shù)組被修改?

如果大家有想出更好的解法,歡迎留言告訴我。

【續(xù)】iOS 面試題(八):實(shí)現(xiàn)一個(gè)嵌套數(shù)組的迭代器

昨天我的代碼,有一個(gè) Bug,就是我沒有處理好嵌套的數(shù)組元素為空的情況,我寫了一個(gè)簡(jiǎn)單的 TestCase,大家也可以試試自己的代碼是否處理好了這種情況:

- (void)testEmptyArray {? ? NSArray *arr = @[ @[ @[ ]], @[@[ @[ @[ ]]]]];? ? NSArrayIterator *c =[[NSArrayIterator alloc]initWithArray:arr];? ? XCTAssertEqualObjects(nil,[c next]);? ? XCTAssertEqualObjects(nil,[c next]);}

于是乎,我發(fā)現(xiàn)我的代碼可以再優(yōu)化一下,用遞歸的方式來處理空數(shù)組的邏輯似乎是寫起來更簡(jiǎn)單的,于是我優(yōu)化之后的邏輯如下:

判斷棧是否為空,如果為空則返回 nil。

從棧中取出元素,看是否遍歷到了結(jié)尾,如果是的話,則出棧。

判斷第 2 步是否使棧為空,如果為空,則返回 nil。

終于拿到元素了,這一步判斷拿到的元素是否是數(shù)組。

如果是數(shù)組,則重新生成一個(gè)遍歷的 NSArrayIteratorCursor 對(duì)象,放到棧中,并且遞歸調(diào)用自己。

如果不是數(shù)組,就把元素返回,同時(shí)更新索引到下一個(gè)位置。

整個(gè)代碼也變得更短更清楚了一些,如下所示:

next 方法的實(shí)現(xiàn):

- (id)next {//? 1. 判斷棧是否為空,如果為空則返回 nil。if(_stack.count==0) {returnnil;? ? }// 2. 從棧中取出元素,看是否遍歷到了結(jié)尾,如果是的話,則出棧。NSArrayIteratorCursor*c;c= [_stack lastObject];while(c.index ==c.array.count&& _stack.count>0) {? ? ? ? [_stack removeLastObject];c= [_stack lastObject];? ? }// 3. 判斷第2步是否使棧為空,如果為空,則返回 nil。if(_stack.count==0) {returnnil;? ? }// 4. 終于拿到元素了,這一步判斷拿到的元素是否是數(shù)組。id item =c.array[c.index];if([item isKindOfClass:[NSArrayclass]]){c.index++;// 5. 如果是數(shù)組,則重新生成一個(gè)遍歷的//? ? NSArrayIteratorCursor 對(duì)象,放到棧中, 然后遞歸調(diào)用 next 方法[selfsetupStackWithArray:item];return[selfnext];? ? }// 6. 如果到了這一步,說明拿到了一個(gè)非數(shù)組的元素,這樣就可以把元素返回,//? ? 同時(shí)更新索引到下一個(gè)位置。c.index++;returnitem;}初使化部分:- (id)initWithArray:(NSArray*)array {self= [superinit];if(self) {? ? ? ? _originArray = array;? ? ? ? _stack = [NSMutableArrayarray];? ? ? ? [selfsetupStackWithArray:array];? ? }returnself;}- (void)setupStackWithArray:(NSArray*)array {NSArrayIteratorCursor*c= [[NSArrayIteratorCursoralloc] initWithArray:array];? ? [_stack addObject:c];}

iOS 面試題(九):創(chuàng)建一個(gè)可以被取消執(zhí)行的 block

問題:

我們知道 block 默認(rèn)是不能被取消掉的,請(qǐng)你封裝一個(gè)可以被取消執(zhí)行的 block wrapper 類,它的定義如下:

typedefvoid(^Block)();@interfaceCancelableObject:NSObject- (id)initWithBlock:(Block)block;- (void)start;- (void)cancel;@end

答案:這道題是從網(wǎng)上看到的,原題是創(chuàng)建一個(gè)可以取消執(zhí)行的 block,我想到兩種寫法。

// 方法一:創(chuàng)建一個(gè)類,將要執(zhí)行的 block 封裝起來,然后類的內(nèi)部有一個(gè) _isCanceled 變量,在執(zhí)行的時(shí)候,檢查這個(gè)變量,如果 _isCanceled 被設(shè)置成 YES 了,則退出執(zhí)行。typedefvoid(^Block)();@interfaceCancelableObject:NSObject- (id)initWithBlock:(Block)block;- (void)start;- (void)cancel;@end@implementationCancelableObject{BOOL_isCanceled;? ? Block _block;}- (id)initWithBlock:(Block)block {self= [superinit];if(self!=nil) {? ? ? ? _isCanceled =NO;? ? ? ? _block = block;? ? }returnself;}- (void)start {? ? __weaktypeof(self) weakSelf =self;dispatch_async(dispatch_get_global_queue(0,0),? ? ? ? ^{if(weakSelf) {typeof(self) strongSelf = weakSelf;if(!strongSelf->_isCanceled) {? ? ? ? ? ? ? (strongSelf->_block)();? ? ? ? ? }? ? ? ? }? ? });}- (void)cancel {? ? _isCanceled =YES;}@end

// 另外一種寫法,將要執(zhí)行的 block 直接放到執(zhí)行隊(duì)列中,但是讓其在執(zhí)行前檢查另一個(gè) isCanceled 的變量,然后把這個(gè)變量的修改實(shí)現(xiàn)在另一個(gè) block 方法中,如下所示:typedefvoid(^CancelableBlock)();typedefvoid(^Block)();+ (CancelableBlock)dispatch_async_with_cancelable:(Block)block {? ? __blockBOOLisCanceled =NO;? ? CancelableBlock cb = ^() {? ? ? ? isCanceled =YES;? ? };dispatch_async(dispatch_get_global_queue(0,0), ^{if(!isCanceled) {? ? ? ? ? ? block();? ? ? ? }? ? });returncb;}

以上兩種方法都只能在 block 執(zhí)行前有效,如果要在 block 執(zhí)行中有效,只能讓 block 在執(zhí)行中,有一個(gè)機(jī)制來定期檢查外部的變量是否有變化,而要做到這一點(diǎn),需要改 block 執(zhí)行中的代碼。在本例中,如果 block 執(zhí)行中的代碼是通過參數(shù)傳遞進(jìn)來的話,似乎并沒有什么辦法可以修改它了。

iOS 面試題(十):一個(gè) Objective-C 對(duì)象的內(nèi)存結(jié)構(gòu)是怎樣的?

問題:一個(gè) Objective-C 對(duì)象的內(nèi)存結(jié)構(gòu)是怎樣的?

答案:這是一道老題,或許很多人都準(zhǔn)備過,其實(shí)如果不是被每個(gè)公司都考查的話,這道題可以看看候選人對(duì)于 iOS 背后底層原理的感興趣程度。真正對(duì)編程感興趣的同學(xué),都會(huì)對(duì)這個(gè)多少有一些好奇,進(jìn)而在網(wǎng)上搜索并學(xué)習(xí)這方面的資料。

以下是本題的簡(jiǎn)單回答:

如果把類的實(shí)例看成一個(gè)C語(yǔ)言的結(jié)構(gòu)體(struct),它首先包含的是一個(gè) isa 指針,而類的其它成員變量依次排列在結(jié)構(gòu)體中。排列順序如下圖所示:

為了驗(yàn)證該說法,我們?cè)赬code中新建一個(gè)工程,在main.m中運(yùn)行如下代碼:

#import@interfaceFather:NSObject{int_father;}@end@implementationFather@end@interfaceChild:Father{int_child;}@end@implementationChild@endintmain(intargc,char* argv[]){? Child * child = [[Child alloc] init];@autoreleasepool{// ...}}// 我們將斷點(diǎn)下在 @autoreleasepool 處,然后在Console中輸入p *child,則可以看到Xcode輸出如下內(nèi)容,這與我們上面的說法一致。(lldb) p *child(Child) $0= {? (Father) Father = {? ? (NSObject)NSObject= {? ? ? (Class) isa = Child? ? }? ? (int) _father =0}? (int) _child =0}

因?yàn)閷?duì)象在內(nèi)存中的排布可以看成一個(gè)結(jié)構(gòu)體,該結(jié)構(gòu)體的大小并不能動(dòng)態(tài)變化。所以無法在運(yùn)行時(shí)動(dòng)態(tài)給對(duì)象增加成員變量。

注:需要特別說明一下,通過 objc_setAssociatedObject 和 objc_getAssociatedObject方法可以變相地給對(duì)象增加成員變量,但由于實(shí)現(xiàn)機(jī)制不一樣,所以并不是真正改變了對(duì)象的內(nèi)存結(jié)構(gòu)。

iOS 面試題(11):對(duì)象內(nèi)存結(jié)構(gòu)中的 isa 指針是用來做什么的?

問題:Objective-C 對(duì)象內(nèi)存結(jié)構(gòu)中的 isa 指針是用來做什么的,有什么用?

答案:Objective-C 是一門面向?qū)ο蟮木幊陶Z(yǔ)言。每一個(gè)對(duì)象都是一個(gè)類的實(shí)例。在 Objective-C 語(yǔ)言的內(nèi)部,每一個(gè)對(duì)象都有一個(gè)名為 isa 的指針,指向該對(duì)象的類。每一個(gè)類描述了一系列它的實(shí)例的特點(diǎn),包括成員變量的列表,成員函數(shù)的列表等。每一個(gè)對(duì)象都可以接受消息,而對(duì)象能夠接收的消息列表是保存在它所對(duì)應(yīng)的類中。

在 Xcode 中按Shift + Command + O, 然后輸入 NSObject.h 和 objc.h,可以打開 NSObject 的定義頭文件,通過頭文件我們可以看到,NSObject 就是一個(gè)包含 isa 指針的結(jié)構(gòu)體,如下圖所示:

按照面向?qū)ο笳Z(yǔ)言的設(shè)計(jì)原則,所有事物都應(yīng)該是對(duì)象(嚴(yán)格來說 Objective-C 并沒有完全做到這一點(diǎn),因?yàn)樗邢?int, double 這樣的簡(jiǎn)單變量類型,而 Swift 語(yǔ)言,連 int 變量也是對(duì)象)。在 Objective-C 語(yǔ)言中,每一個(gè)類實(shí)際上也是一個(gè)對(duì)象。每一個(gè)類也有一個(gè)名為 isa 的指針。每一個(gè)類也可以接受消息,例如代碼[NSObject alloc],就是向 NSObject 這個(gè)類發(fā)送名為alloc消息。

在 Xcode 中按Shift + Command + O, 然后輸入 runtime.h,可以打開 Class 的定義頭文件,通過頭文件我們可以看到,Class 也是一個(gè)包含 isa 指針的結(jié)構(gòu)體,如下圖所示。(圖中除了 isa 外還有其它成員變量,但那是為了兼容非 2.0 版的 Objective-C 的遺留邏輯,大家可以忽略它。)

因?yàn)轭愐彩且粋€(gè)對(duì)象,那它也必須是另一個(gè)類的實(shí)列,這個(gè)類就是元類 (metaclass)。元類保存了類方法的列表。當(dāng)一個(gè)類方法被調(diào)用時(shí),元類會(huì)首先查找它本身是否有該類方法的實(shí)現(xiàn),如果沒有,則該元類會(huì)向它的父類查找該方法,直到一直找到繼承鏈的頭。

元類 (metaclass) 也是一個(gè)對(duì)象,那么元類的 isa 指針又指向哪里呢?為了設(shè)計(jì)上的完整,所有的元類的 isa 指針都會(huì)指向一個(gè)根元類 (root metaclass)。根元類 (root metaclass) 本身的 isa 指針指向自己,這樣就行成了一個(gè)閉環(huán)。上面提到,一個(gè)對(duì)象能夠接收的消息列表是保存在它所對(duì)應(yīng)的類中的。在實(shí)際編程中,我們幾乎不會(huì)遇到向元類發(fā)消息的情況,那它的 isa 指針在實(shí)際上很少用到。不過這么設(shè)計(jì)保證了面向?qū)ο蟾拍钤?Objective-C 語(yǔ)言中的完整,即語(yǔ)言中的所有事物都是對(duì)象,都有 isa 指針。

我們?cè)賮砜纯蠢^承關(guān)系,由于類方法的定義是保存在元類 (metaclass) 中,而方法調(diào)用的規(guī)則是,如果該類沒有一個(gè)方法的實(shí)現(xiàn),則向它的父類繼續(xù)查找。所以,為了保證父類的類方法可以在子類中可以被調(diào)用,所以子類的元類會(huì)繼承父類的元類,換而言之,類對(duì)象和元類對(duì)象有著同樣的繼承關(guān)系。

我很想把關(guān)系說清楚一些,但是這塊兒確實(shí)有點(diǎn)繞,我們還是來看圖吧,很多時(shí)候圖象比文字表達(dá)起來更為直觀。下面這張圖或許能夠讓大家對(duì) isa 和繼承的關(guān)系清楚一些:

我們可以從圖中看出:

NSObject 的類中定義了實(shí)例方法,例如 -(id)init 方法 和 - (void)dealloc 方法。

NSObject 的元類中定義了類方法,例如 +(id)alloc 方法 和 + (void)load 、+ (void)initialize 方法。

NSObject 的元類繼承自 NSObject 類,所以 NSObject 類是所有類的根,因此 NSObject 中定義的實(shí)例方法可以被所有對(duì)象調(diào)用,例如 - (id)init 方法 和 - (void)dealloc 方法。

NSObject 的元類的 isa 指向自己。

isa swizzling 的應(yīng)用

系統(tǒng)提供的 KVO 的實(shí)現(xiàn),就利用了動(dòng)態(tài)地修改 isa 指針的值的技術(shù)。在 蘋果的文檔

中可以看到如下描述:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

iOS 面試題(12):按層遍歷二叉樹的節(jié)點(diǎn)

解題代碼都是使用 Swift 完成的,我也盡量在代碼中使用了 Swift 語(yǔ)言的一些特性,大家可以順便學(xué)學(xué) Swift。

問題:給你一棵二叉樹,請(qǐng)按層輸出其的節(jié)點(diǎn)值,即:按從上到下,從左到右的順序。

例如,如果給你如下一棵二叉樹:

3/ \920/? \157輸出結(jié)果應(yīng)該是:[? [3],? [9,20],? [15,7]]本題的Swift代碼模版如下:privateclassTreeNode{publicvarval:Intpublicvarleft:TreeNode?publicvarright:TreeNode?publicinit(_val:Int) {self.val = valself.left=nilself.right=nil}}classSolution{funclevelOrder(_root: TreeNode?)-> [[Int]] {? ? }}

解答:本題出自LeetCode第 102 題,是一個(gè)典型的有關(guān)遍歷的題目。為了按層遍歷,我們需要使用「隊(duì)列」,來將每一層的節(jié)點(diǎn)先保存下來,然后再依次處理。

因?yàn)槲覀儾坏枰磳觼肀闅v,還需要按層來輸出結(jié)果,所以我在代碼中使用了兩個(gè)隊(duì)列,分別名為 level 和 nextLevel,用于保存不同層的節(jié)點(diǎn)。

最終,整個(gè)算法邏輯是:

判斷輸入?yún)?shù)是否是為空。

將根節(jié)點(diǎn)加入到隊(duì)列 level 中。

如果 level 不為空,則:

3.1 將 level 加入到結(jié)果 ans 中。

3.2 遍歷 level 的左子節(jié)點(diǎn)和右子節(jié)點(diǎn),將其加入到 nextLevel 中。

3.3 將 nextLevel 賦值給 level,重復(fù)第 3 步的判斷。

將 ans 中的節(jié)點(diǎn)換成節(jié)點(diǎn)的值,返回結(jié)果。

因?yàn)槲覀兪怯?Swift 來實(shí)現(xiàn)代碼,所以我使用了一些 Swift 語(yǔ)言的特性。例如:隊(duì)列中我們保存的是節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu),但是最終輸出的時(shí)候,我們需要輸出的是值,在代碼中,我使用了 Swift 的函數(shù)式的鏈?zhǔn)秸{(diào)用,將嵌套數(shù)組中的元素類型做了一次變換,如下所示:

letans = result.map{ $0.map{ $0.val }}另外,我們也使用了Swift特有的guard關(guān)鍵字,來處理參數(shù)的特殊情況。完整的參考代碼如下:////? Binary Tree Level Order Traversal.swift////? Created by Tang Qiao.//importFoundationprivateclassTreeNode{publicvarval:Intpublicvarleft:TreeNode?publicvarright:TreeNode?publicinit(_val:Int) {self.val = valself.left=nilself.right=nil}}privateclassSolution{funclevelOrder(_root: TreeNode?)-> [[Int]] {guardletroot = rootelse{return[]? ? ? ? }varresult = [[TreeNode]]()varlevel = [TreeNode]()? ? ? ? level.append(root)whilelevel.count!=0{? ? ? ? ? ? result.append(level)varnextLevel = [TreeNode]()fornodeinlevel {ifletleftNode = node.left{? ? ? ? ? ? ? ? ? ? nextLevel.append(leftNode)? ? ? ? ? ? ? ? }ifletrightNode = node.right{? ? ? ? ? ? ? ? ? ? nextLevel.append(rightNode)? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ? level = nextLevel? ? ? ? }letans = result.map{ $0.map{ $0.val }}returnans? ? }}

微信中排版代碼非常不便,所以上述代碼也可以從我的 Gist 中找到:代碼Gist地址

完成這道題的同學(xué),可以試著練習(xí)一下LeetCode的第 107 題,看看能不能只改動(dòng)一行代碼,就把 107 題也解決掉。

iOS 面試題(13):求兩個(gè)鏈表表示的數(shù)的和

問題:給你兩個(gè)鏈表,分別表示兩個(gè)非負(fù)的整數(shù)。每個(gè)鏈表的節(jié)點(diǎn)表示一個(gè)整數(shù)位。

為了方便計(jì)算,整數(shù)的低位在鏈表頭,例如:123 在鏈表中的表示方式是:

3->2->1

現(xiàn)在給你兩個(gè)這樣結(jié)構(gòu)的鏈表,請(qǐng)輸出它們求和之后的結(jié)果。例如:

輸入: (2->4->1) + (5->6->1)輸出:7->0->3

本題的 Swift 代碼模版如下:

privateclassListNode{publicvarval:Intpublicvarnext:ListNode?publicinit(_val:Int) {self.val = valself.next =nil}}classSolution{funcaddTwoNumbers(_l1: ListNode?,_l2: ListNode?)->ListNode? {? ? }}

考查點(diǎn):本題出自 LeetCode 上的第 2 題。

這是我高中學(xué)習(xí)編程時(shí)最早接觸的一類題目,我們把這類題目叫做「高精度計(jì)算」,其實(shí)就是在計(jì)算機(jī)計(jì)算精度不夠時(shí),模擬我們?cè)诩埳涎菟愕姆绞絹碛?jì)算答案,然后獲得足夠精度的解。

我還記得我 7 年前第一次去網(wǎng)易有道面試的時(shí)候,就考查的是一道類似的高精度計(jì)算題目,比這道題復(fù)雜得多,我當(dāng)時(shí)用了一個(gè)比較笨的辦法,加上當(dāng)時(shí)還是用 C++ 寫的,內(nèi)存分配和釋放寫起來也比較麻煩,最后寫了兩頁(yè) A4 紙才寫完。

這道題其實(shí)完全不考查什么「算法」,人人都知道怎么計(jì)算,但是它考察了「將想法轉(zhuǎn)換成代碼」的能力,新手通常犯的毛病就是:意思都明白,但是寫不出來代碼。所以,這類題目用來過濾菜鳥確實(shí)是挺有效的辦法。

答案:本題的做法其實(shí)沒什么特別,就是直接計(jì)算。計(jì)算的時(shí)候需要考慮到以下這些情況:

兩個(gè)整數(shù)長(zhǎng)度不一致的情況。

進(jìn)位的情況。當(dāng)進(jìn)位產(chǎn)生時(shí),我們需要保存一個(gè)標(biāo)志位,以便在計(jì)算下一位的和的時(shí)候,加上進(jìn)位。

當(dāng)計(jì)算完后,如果還有進(jìn)位,需要處理最后結(jié)果加一位的情況。

以下是完整的代碼,我使用了一些 Swift 語(yǔ)言的特性,比如用 flatMap 來減少對(duì)于 Optional 類型值為 nil 的判斷。

privateclassListNode{publicvarval:Intpublicvarnext:ListNode?publicinit(_val:Int) {self.val = valself.next =nil}}privateclassSolution{privatefuncgetNodeValue(_node: ListNode?)->Int{returnnode.flatMap { $0.val } ??0}funcaddTwoNumbers(_l1: ListNode?,_l2: ListNode?)->ListNode? {ifl1 ==nil|| l2 ==nil{returnl1 ?? l2? ? ? ? }varp1 = l1varp2 = l2letresult:ListNode? =ListNode(0)varcurrent = resultvarextra =0whilep1 !=nil|| p2 !=nil|| extra !=0{vartot = getNodeValue(p1) +? ? ? ? ? ? ? ? ? ? ? getNodeValue(p2) + extra? ? ? ? ? ? extra = tot /10tot = tot %10letsum:ListNode? =ListNode(tot)? ? ? ? ? ? current!.next = sum? ? ? ? ? ? current = sum? ? ? ? ? ? p1 = p1.flatMap { $0.next }? ? ? ? ? ? p2 = p2.flatMap { $0.next }? ? ? ? }returnresult!.next? ? }}

以上代碼也可以從我的 Gist 中找到:Gist

偷偷告訴你一個(gè)小秘密,Gist 里面的代碼我稍微修改了兩行,最終性能就從打敗 LeetCode 10% 的提交變成了打敗 LeetCode 50% 的提交,如果你感興趣,可以自己仔細(xì)對(duì)比一下。

iOS 面試題(14):計(jì)算有多少個(gè)島嶼

問題:在一個(gè)地圖中,找出一共有多少個(gè)島嶼。

我們用一個(gè)二維數(shù)組表示這個(gè)地圖,地圖中的 1 表示陸地,0 表示水域。一個(gè)島嶼是指由上下左右相連的陸地,并且被水域包圍的區(qū)域。

你可以假設(shè)地圖的四周都是水域。

例一:一共有1個(gè)島嶼。11110110101100000000

例二:一共有3個(gè)島嶼。11000110000010000011

答案:這是 LeetCode 上的 第 200 題,我們可以用一種被稱為「種子填充」(floodfill)的辦法來解決此題。

具體的做法是:

遍歷整個(gè)地圖,找到一個(gè)未被標(biāo)記過的,值為 1 的坐標(biāo)。

從這個(gè)坐標(biāo)開始,從上下左右四個(gè)方向,標(biāo)記相鄰的 1 。

把這些相鄰的坐標(biāo),都標(biāo)記下來,遞歸的進(jìn)行標(biāo)記,以便把相鄰的相鄰塊也能標(biāo)記上。

待標(biāo)記全部完成之后,將島嶼的計(jì)數(shù) +1。

回到第 1 步。如果第 1 步無法找到未標(biāo)記的坐標(biāo),則結(jié)束。

雖然思路簡(jiǎn)單,但是實(shí)現(xiàn)起來代碼量也不算小。這里有一些小技巧:

我們可以將上下左右四個(gè)方向的偏移量保存在數(shù)組中,這樣在計(jì)算位置的時(shí)候,寫起來更簡(jiǎn)單一些。

遞歸的標(biāo)記過程可以用深度優(yōu)先搜索(DFS)或者寬度優(yōu)先搜索(BFS)。

以下是完整的參考代碼:

privateclassSolution{privatevarflag: [[Int]]privatevaranswer:Intprivatevarmovex : [Int] {return[-1,1,0,0]? ? }privatevarmovey : [Int] {return[0,0, -1,1]? ? }init() {? ? ? ? flag = [[Int]]()? ? ? ? answer =0}funcdfs(_grid: [[Character]] ,_x: Int,_y: Int){foriin0..<4{lettox = x + movex[i]lettoy = y + movey[i]iftox >=0&& tox < grid.count&& toy >=0&& toy < grid[0].count&& grid[tox][toy] =="1"&& flag[tox][toy] ==0{? ? ? ? ? ? flag[tox][toy] =1dfs(grid, tox, toy)? ? ? ? ? ? }? ? ? ? }? ? }funcnumIslands(_grid: [[Character]])->Int{? ? ? ? answer =0flag = grid.map{? ? ? ? ? ? $0.map{_inreturn0}}foriin0..

Swift 的參數(shù)默認(rèn)是不能修改值的,但是如果是 C++ 語(yǔ)言的話,我們可以直接在地圖上做標(biāo)記。因?yàn)榈貓D只有 0 和 1 兩種值,我們可以用 2 表示「標(biāo)記過的陸地」,這樣就省略了額外的標(biāo)記數(shù)組。以下是我寫的一個(gè) C++ 的示例程序:

class Solution {public:? ? void fillLands(vector>&grid,intpx,intpy) {intmovex[] = {0,0,1,-1};intmovey[] = {-1,1,0,0};? ? ? ? queue> q;? ? ? ? q.push(make_pair(px, py));grid[px][py] ='2';while(!q.empty()) {? ? ? ? ? ? pair item = q.front();? ? ? ? ? ? q.pop();inttox, toy;for(inti =0; i <4; ++i) {? ? ? ? ? ? ? ? tox = item.first + movex[i];? ? ? ? ? ? ? ? toy = item.second + movey[i];if(tox >=0&& tox =0&& toy >&grid) {intans =0;for(inti =0; i

iOS 面試題(16):解釋垃圾回收的原理

摘要: 問題 我們知道,Android 手機(jī)通常使用 Java 來開發(fā),而 Java 是使用垃圾回收這種內(nèi)存管理方式。 那么,ARC 和垃圾回收對(duì)比,有什么優(yōu)點(diǎn)和缺點(diǎn)? 考查點(diǎn) 此題其實(shí)是考查大家的知識(shí)面,雖然做 iOS 開發(fā)并不需要用到垃圾回收這種內(nèi)存管理…

問題

我們知道,Android 手機(jī)通常使用 Java 來開發(fā),而 Java 是使用垃圾回收這種內(nèi)存管理方式。 那么,ARC 和垃圾回收對(duì)比,有什么優(yōu)點(diǎn)和缺點(diǎn)?

考查點(diǎn)

此題其實(shí)是考查大家的知識(shí)面,雖然做 iOS 開發(fā)并不需要用到垃圾回收這種內(nèi)存管理機(jī)制。但是垃圾回收被使用得非常普遍,不但有 Java,還包括 JavaScript, C#,Go 等語(yǔ)言。

如果兩個(gè)候選人,一個(gè)人只會(huì) iOS 開發(fā),另一個(gè)人不但會(huì) iOS 開發(fā),對(duì)別的語(yǔ)言或技術(shù)也有興趣了解,那我通常更傾向于后者。而且事實(shí)常常是,由于后者對(duì)計(jì)算機(jī)興趣更濃,他在 iOS 上也通常專研得比前者更多。

垃圾回收簡(jiǎn)介

作為 iOS 開發(fā)者,了解一下這個(gè)世界上除了 ARC 之外最流行的內(nèi)存管理方式,還是挺有價(jià)值的。所以我盡量簡(jiǎn)單給大家介紹一下。

垃圾回收(Garbage Collection,簡(jiǎn)稱 GC)這種內(nèi)存管理機(jī)制最早由圖靈獎(jiǎng)獲得者 John McCarthy 在 1959 年提出,垃圾回收的理論主要基于一個(gè)事實(shí):大部分的對(duì)象的生命期都很短。

所以,GC 將內(nèi)存中的對(duì)象主要分成兩個(gè)區(qū)域:Young 區(qū)和 Old 區(qū)。對(duì)象先在 Young 區(qū)被創(chuàng)建,然后如果經(jīng)過一段時(shí)間還存活著,則被移動(dòng)到 Old 區(qū)。(其實(shí)還有一個(gè) Perm 區(qū),但是內(nèi)存回收算法通常不涉及這個(gè)區(qū)域)

Young 區(qū)和 Old 區(qū)因?yàn)閷?duì)象的特點(diǎn)不一樣,所以采用了兩種完全不同的內(nèi)存回收算法。

Young 區(qū)的對(duì)象因?yàn)榇蟛糠稚诙己芏蹋看位厥罩笾挥猩俨糠帜軌虼婊睿圆捎玫乃惴ń?Copying 算法,簡(jiǎn)單說來就是直接把活著的對(duì)象復(fù)制到另一個(gè)地方。Young 區(qū)內(nèi)部又分成了三塊區(qū)域:Eden 區(qū) , From 區(qū) , To 區(qū)。每次執(zhí)行 Copying 算法時(shí),即將存活的對(duì)象從 Eden 區(qū)和 From 區(qū)復(fù)制到 To 區(qū),然后交換 From 區(qū)和 To 區(qū)的名字(即 From 區(qū)變成 To 區(qū),To 區(qū)變成 From 區(qū))。

Old 區(qū)的對(duì)象因?yàn)槎际谴婊钕聛淼睦纤緳C(jī)了,所以如果用 Copying 算法的話,很可能 90% 的對(duì)象都得復(fù)制一遍了,不劃算啊!所以 Old 區(qū)的回收算法叫 Mark-Sweep 算法。簡(jiǎn)單來說,就是只是把不用的對(duì)象先標(biāo)記(Mark)出來,然后回收(Sweep),活著的對(duì)象就不動(dòng)它了。因?yàn)榇蟛糠謱?duì)象都活著,所以回收下來的對(duì)象并不多。但是這個(gè)算法會(huì)有一個(gè)問題:它會(huì)產(chǎn)生內(nèi)存碎片,所以它一般還會(huì)帶有整理內(nèi)存碎片的邏輯,在算法中叫做 Compact。如何整理呢?早年用過 Windows 的硬盤碎片整理程序的朋友可能能理解,其實(shí)就是把對(duì)象插到這些空的位置里。這里面還涉及很多優(yōu)化的細(xì)節(jié),我就不一一展開了。

講完主要的算法,接下來 GC 需要解決的問題就只剩下如何找出需要回收的垃圾對(duì)象了。為了避免 ARC 解決不了的循環(huán)引用問題,GC 引入了一個(gè)叫做「可達(dá)性」的概念,應(yīng)用這個(gè)概念,即使是有循環(huán)引用的垃圾對(duì)象,也可以被回收掉。下面就給大家介紹一下這個(gè)概念。

當(dāng) GC 工作時(shí),GC 認(rèn)為當(dāng)前的一些對(duì)象是有效的,這些對(duì)象包括:全局變量,棧里面的變量等,然后 GC 從這些變量出發(fā),去標(biāo)記這些變量「可達(dá)」的其它變量,這個(gè)標(biāo)記是一個(gè)遞歸的過程,最后就像從樹根的內(nèi)存對(duì)象開始,把所有的樹枝和樹葉都記成可達(dá)的了。那除了這些「可達(dá)」的變量,別的變量就都需要被回收了。

聽起來很牛逼對(duì)不對(duì)?那為什么蘋果不用呢?實(shí)際上蘋果在 OS X 10.5 的時(shí)候還真用了,不過在 10.7 的時(shí)候把 GC 換成了 ARC。那么,GC 有什么問題讓蘋果不能忍,這就是:垃圾回收的時(shí)候,整個(gè)程序需要暫停,英文把這個(gè)過程叫做:Stop the World。所以說,你知道 Android 手機(jī)有時(shí)候?yàn)槭裁磿?huì)卡吧,GC 就相當(dāng)于春運(yùn)的最后一天返城高峰。當(dāng)所有的對(duì)象都需要一起回收時(shí),那種體驗(yàn)肯定是當(dāng)時(shí)還在世的喬布斯忍受不了的。

看看下面這幅漫畫,真實(shí)地展現(xiàn)出 GC 最尷尬的情況(漫畫中提到的 Full GC,就是指執(zhí)行 Old 區(qū)的內(nèi)存回收):

當(dāng)然,事實(shí)上經(jīng)過多年的發(fā)展,GC 的回收算法一直在被優(yōu)化,人們想了各種辦法來優(yōu)化暫停的時(shí)間,所以情況并沒有那么糟糕。

答案

ARC 相對(duì)于 GC 的優(yōu)點(diǎn):

ARC 工作在編譯期,在運(yùn)行時(shí)沒有額外開銷。

ARC 的內(nèi)存回收是平穩(wěn)進(jìn)行的,對(duì)象不被使用時(shí)會(huì)立即被回收。而 GC 的內(nèi)存回收是一陣一陣的,回收時(shí)需要暫停程序,會(huì)有一定的卡頓。

ARC 相對(duì)于 GC 的缺點(diǎn):

GC 真的是太簡(jiǎn)單了,基本上完全不用處理內(nèi)存管理問題,而 ARC 還是需要處理類似循環(huán)引用這種內(nèi)存管理問題。

GC 一類的語(yǔ)言相對(duì)來說學(xué)習(xí)起來更簡(jiǎn)單。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,774評(píng)論 0 9
  • 【2017年最新】? iOS面試題及答案 設(shè)計(jì)模式是什么? 你知道哪些設(shè)計(jì)模式,并簡(jiǎn)要敘述? 設(shè)計(jì)模式是一種編碼經(jīng)...
    紫色冰雨閱讀 623評(píng)論 0 1
  • [※]@property中有哪些屬性關(guān)鍵字? 有3類。ARC下默認(rèn)是(atomic, assign, readwr...
    Icyjade_White閱讀 298評(píng)論 0 1
  • 第五天…… 提前了兩天開學(xué)啊,教育局啊,你為何如此不公啊,為何給人以希望又毀之以失望啊T^T
    愛雪的雨閱讀 168評(píng)論 0 0
  • 大一的暑假除了沒有對(duì)自己高考分?jǐn)?shù)的擔(dān)憂,對(duì)填志愿的茫然無措和成為萌新口中的學(xué)長(zhǎng)學(xué)姐外,似乎沒有什么不同。該...
    皖南醫(yī)學(xué)院閱讀 290評(píng)論 0 0