《iOS 面试之道》是故胤道长和唐巧 2018 年合著的针对面试一些问题的书,买之后虽有翻阅,但是始终未认真通读。现在想把书中对于我来说有价值的知识点,简短的总结一下。这里分两篇来说,这篇主要是介绍一些算法基础。
《iOS 面试之道》两篇总结之一:开发技能篇
《iOS 面试之道》两篇总结之二:算法基础篇
# 基本的数据结构
# 数组
ContiguousArray: 效率最高,元素分配在连续的内存地址上。如果数组元素是值类型,Swift 自动调用这种实现。处理大量元素时,推荐这种方式。
Array: 自动桥接到 Objective-C 的 NSArray 上,如果元素时值类型,与 ContiguousArray 性能无差别。
ArraySlice: 它不是一个新的数组,而只是一个片段,在内存上与数组享用同一区域。
下面用数组实现一个栈操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Stack { var stack: [AnyObject ] var isEmpty: Bool { return stack.isEmpty } var peel: AnyObject ? { return stack.last } init () { stack = [AnyObject ]() } func push (object : AnyObject ) { stack.append(object) } func pop () -> AnyObject ? { if ! isEmpty { return stack.removeLast() } else { return nil } } }
最后强调一个操作: reserveCapacity()
。用于为原数组预留空间,防止数组数组添加或删除元素时反复申请内存空间或创建新数组。再别在数组创建或者 removeAll()
时,对整段代码起到性能提升的作用。
# 字典和集合
一般字典和集合的 Key 都要遵循 Hashable 协议,自定义的类型作为 Key 实现 Hashable。这里说下集合的一些特点:
1 2 3 4 5 6 7 let primeNums: Set = [3 , 5 , 7 , 11 , 13 ]let oddNums: Set = [1 , 3 , 5 , 7 , 9 ]let primeAndOddNum = primeNums.intersection(oddNums)var orNums = primeNumsorNums.formUnion(oddNums) let primeNotOddNum = primeNums.subtracting(oddNums)
这里一个字典的例子非常有意思:
1 2 3 4 5 Dictionary ("hello" .map { ($0 , 1 )}, uniquingKeysWith: + )
算法题: 给出一个整型数组和一个目标值,判断数组中是否有两个数之和等于目标值。
可利用目标减去差值的方式,时间复杂度为 O(n) :
1 2 3 4 5 6 7 8 9 10 func twoNum (nums : [Int ], _ target : Int ) -> Bool { var set = Set <Int >() for num in nums { if set .contains(target - num) { return true } set .insert(num) } return false }
如果修改这个题:给出一个整型数组有且仅有两个数之和等于目标值,求这两个数在数组中的序号。
这里使用字典记录序号,时间复杂度依然为 O(n) :
1 2 3 4 5 6 7 8 9 10 11 func twoNum (nums : [Int ], _ target : Int ) -> [Int ] { var dic = [Int : Int ]() for (index, value) in nums.enumerated() { if let lastIndex = dic[target - value] { return [index, lastIndex] } else { dic[value] = index } } }
# 字符串
# 给出一个字符串,要求其按照单词顺序进行反转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 override func viewDidLoad () { super .viewDidLoad() print (reverseWords(s: "the sky is blue" ) ?? "None" ) } private func reverseWords (s : String ?) -> String ? { guard let s = s else { return nil } var chars = s.map { $0 } reverse(& chars, 0 , chars.count - 1 ) print (chars) var start = 0 for i in 0 ..< chars.count { if i == chars.count - 1 || chars[i + 1 ] == " " { reverse(& chars, start, i) start = i + 2 } } return String (chars) } private func reverse <T >(_ chars : inout [T ], _ start : Int , _ end : Int ) { var start = start var end = end while start < end { swap (& chars, start, end) start += 1 end -= 1 } } private func swap <T >(_ chars : inout [T ], _ p : Int , _ q : Int ) { (chars[p], chars[q]) = (chars[q], chars[p]) }
输出:
1 2 ["e" , "u" , "l" , "b" , " " , " " , " " , " " , " " , " " , "s" , "i" , " " , " " , " " , " " , " " , "y" , "k" , "s" , " " , "e" , "h" , "t" ] Optional ("blue is sky the" )
# 链表
# 链表的基本概念
这里不再赘述链表基本概念,直接实现链表结点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class ListNode { var val: Int var next: ListNode ? init (_ val : Int ) { self .val = val self .next = nil } } class List { var head: ListNode ? var tail: ListNode ? func appendToTail (_ val : Int ) { if tail == nil { tail = ListNode (val) head = tail } else { tail? .next = ListNode (val) tail = tail? .next } } func appendToHead (_ val : Int ) { if head == nil { head = ListNode (val) tail = head } else { let temp = ListNode (val) temp.next = head head = temp } } }
# Dummy 节点和尾插发
给出一个链表和一个值 x,要求将链表中所有小于 x 的值放左边,所有大于 x 的放右边,并且原始链表的节点顺序不变。
Dummy 节点作用:它的作用就是作为一个虚拟的头结点。因为我们不知道要返回的新链表的节点是哪一个,甚至不知道是否存在。因为 Dummy 节点可以巧妙涵盖各种情况。可以用 dummy.next 方便的返回最终需要的头节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func partition (_ head : ListNode ?, _ x : Int ) -> ListNode ? { let prevDummy = ListNode (0 ) let postDummy = ListNode (0 ) var prev = prevDummy, post = postDummy var node = head while node != nil { if node! .val < x { prev.next = node prev = node! } else { post.next = node! post = node! } node = node! .next } post.next = nil prev.next = postDummy.next return prevDummy.next }
# 快行指针
快行指针用来检测链表是否有环。可以简单理解为:两个指针同时访问链表,但是移动速度不一样,如果存在环,跑的快的总有机会再次遇到跑的慢的。如果跑的快的到头了,还没见到跑的慢的,则不存在环,就好像操场跑步一样。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func hadCycle (_ head : ListNode ?) -> Bool { var slow = head var faster = head while faster != nil && faster? .next != nil { slow = slow? .next faster = faster? .next? .next if slow === faster { return true } } return false }
注意这里用了三个等号。表示恒等。
下面使用快行指针解决一个问题:
删除链表中倒数第 n 个节点,例如:1 -> 2 -> 3 -> 4 -> 5,n = 2,返回 1 -> 2 -> 3 -> 5。给定 n 的长度小于等于链表的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func removeNthFromEnd (head : ListNode ?, _ n : Int ) -> ListNode ? { guard let head = head else { return nil } let dummy = ListNode (0 ) dummy.next = head var prev: ListNode ? = dummy var post: ListNode ? = dummy for _ in 0 ..< n { if post == nil { break } post = post! .next } while post != nil && post? .next != nil { prev = prev! .next post = post! .next } prev! .next = prev! .next! .next return dummy.next }
# 栈和队列
# 栈的基本概念
栈是后进先出的结构,即 FILO,first in last out。
如果 iOS 项目需要添加撤销操作,栈是首选的数据结构。使用数组实现更加方便。
无论面试还是写 App。主要关注栈的几个基本操作:push,pop,isEmpty,peek(栈顶元素),和 size。
下面定义一个栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 protocol Stack { associatedtype Element var isEmpty: Bool { get } var size: Int { get } var peek: Element ? { get } mutating func push (_ newElement : Element ) mutating func pop () -> Element ? } struct IntergerStack : Stack { typealias Element = Int var isEmpty: Bool { return stacks.isEmpty } var size: Int { return stacks.count } var peek: Int ? { return stacks.last } private var stacks = [Element ]() mutating func push (_ newElement : Int ) { stacks.append(newElement) } mutating func pop () -> Int ? { return stacks.popLast() } }
# 队列的基本概念
队列是先进先出的结构,即 FIFO,first in first out。
iOS 开发中的多线程 GCD 以及 NSOperation 都是基于队列实现的。
关于队列只需要关注 enqueue、dequeue、isEmpty、peek 和 size 等操作。
下面定义一个队列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 protocol Queue { associatedtype Element var isEmpty: Bool { get } var size: Int { get } var peek: Element ? { get } mutating func enqueue (_ newElement : Element ) mutating func dequeue () -> Element ? } struct IntegerQueue : Queue { typealias Element = Int var isEmpty: Bool { return left.isEmpty && right.isEmpty } var size: Int { return left.count + right.count } var peek: Int ? { return left.isEmpty ? right.first : left.last } private var left = [Element ]() private var right = [Element ]() mutating func enqueue (_ newElement : Int ) { right.append(newElement) } mutating func dequeue () -> Int ? { if left.isEmpty { left = right.reversed() right.removeAll() } return left.popLast() } }
# 栈和队列实战面试题
给出一个文件的绝对路径,要求将其简化。如:
路径是 “/home/”,简化后为 “/home”。
路径是 “/a/./…/…/c”,简化后为 “/c”。
根据常识,有以下规则:
“.” 代表当前路径。比如 “/a/.” 实际上就是 “/a”,无论输入多少个 “.”(除了 2 个),都返回当前目录。
“…” 代表上一级目录,比如 “a/b/…” 实际就是 “/a”。
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func simplifyPath (path : String ) -> String { var pathStack = [String ]() let paths = path.components(separatedBy: "/" ) for path in paths { print ("path = \(path) " ) guard path != "." else { continue } if path == ".." { if pathStack.count > 0 { pathStack.removeLast() } else { } } else if path != "" { pathStack.append(path) } else { } } print ("stacks = \(pathStack) " ) let res = pathStack.reduce("" ) { totol, dir in "\(totol) /\(dir) " } return res.isEmpty ? "/" : res }
# 二叉树