《iOS面試之道》算法基礎學習(上)

前言

道長和唐巧的面試之道,剛出來第一時間就入手了,也是趁着公司目前不是很忙,能好好靜下心來細讀這本書.筆者認爲這本書的最大亮點就在第二章的算法基礎,因此想經過筆記的形式來記錄算法的學習過程,同時在忘記的時候也能第一時間翻閱查看.html

這部分代碼都是經過Swift來說解的,因此對於想學習Swift的初學者來講也是不錯的.在本章算法題筆者簡單的分析了下,須要看詳細的你們能夠選擇購買原書學習.node

1 字典和集合:給出一個整型數組和一個目標值,判斷數組中是否有兩個數之和等於目標值.

這種題是LeetCode上比較基礎的題,書上關於這題swift的代碼也是比較簡單.面試

func twoSum (nums: [Int], _ target: Int) -> Bool {
      //初始化集合
      var set = Set<Int>()
      //遍歷整型數組
      for num in nums {
          //判斷集合中是否包含目標值減去當前值的結果
          if set.contains(target - num) {
              //包含 返回true
              return true
          }
          //不包含 將當前值存進集合 用做下次判斷
          set.insert(num)
      }
      //都不包含 返回fasle
      return false
  }
複製代碼

這種方法的時間複雜度爲O(n),較選中一個數而後遍歷整個數組這種複雜度爲 O(n^2) 的方法要好不少.算法

1.2:在這道題的基礎上作修改:給定一個整型數組中有且僅有 兩個數之和等於目標值,求這兩個數在數組中的序號.

書中的方法爲了方便獲得序列號,使用了字典,看代碼swift

func twoSum (nums: [Int], _ target: Int) -> [Int] {
    //初始化字典
    var dict = [Int: Int]()
    //經過索引i 和對應的num進行判斷
    for (i,num) in nums.enumerated() {
        //從dict字典中取出以前保存的索引,判斷是否存在索引值
        if let lastIndex = dict[target - num] {
            //返回以前存的索引和當前索引
            return [lastIndex, i]
        }else {
            //保存當前索引,用於後續判斷
            dict[num] = i
        }
    }
    //致命錯誤來停止程序
    fatalError("No valid output!")
}
複製代碼

巧妙的用到了字典的特性,用key表示數組的值,經過判斷字典中是否含有目標值的key來取出索引.數組

2 字符串:給出一個字符串,要求將其按照單詞順序進行反轉.

eg:若是是"hello world",那麼反轉後的結果就是"world hello",這個題比較常規了,可是要注意空格的特殊處理.看代碼:數據結構

fileprivate func _reverse<T>(_ chars: inout[T], _ start: Int, _ end: Int) {
    //接收字符串反轉起始,結束位置
    var start = start, end = end
    //判斷反轉字符串的位置
    while start < end {
        //start end位置 字符互換
        swap(&chars, start, end)
        //往中間位置靠攏
        start += 1
        end -= 1
    }
}

fileprivate func swap<T>(_ chars: inout[T], _ p: Int, _ q: Int){
    //將p,q字符 位置進行互換  這種寫法也是swift裏的一大亮點
    (chars[p], chars[q]) = (chars[q], chars[p])
}
複製代碼

上面方法是實現了字符串的反轉,再來看下每一個單詞的單獨反轉.須要注意的是單詞的反轉須要對空格進行判斷,來看完整實現代碼.app

func reverseWords(s: String?) -> String? {
    //判斷入參s是否有值
    guard let s = s else {
        return nil
    }
    //將字符串s分割成字符數組
    var chars = Array(s.characters), start = 0
    //反轉chars字符數組
    _reverse(&chars, start, chars.count - 1)
   
    for i in 0 ..< chars.count {
        //當i等於 數組最後一位 或者 遇到空格時反轉字符串
        if i == chars.count - 1 || chars[i + 1] == " " {
            //將每一個單獨的單詞進行反轉
            _reverse(&chars, start, i)
            //更新start位置
            start = i + 2
        }
    }
    return String(chars)
}
複製代碼

註解:ide

1:以輸入字符串爲"Hello World"爲例,首先將該字符串分割成一個個的小字符數組,而後反轉成 "dlroW olleH".函數

2「接着咱們經過判斷字符數組中的空格位和最後一位字符,對單一的單詞進行分段反轉,更新start位置. 3:最後輸出咱們須要的結果"World Hello"

其實字符串反轉蘋果已經爲咱們提供了方法來實現這個操做.

func reverseWords(s: String) -> String? {
    //用空格劃分字符串
    let chars = s.components(separatedBy:" ")
    //將字符串數組 反轉 並經過空格從新組合
    let reverseString = chars.reversed().joined(separator:" ")
    return reverseString
}
複製代碼

寫法很是的簡單,就不贅述了.可是components和joined的時間複雜度是高於上面的寫法的,須要注意.

拓展

若是說把上面的字符串換成整數,又該如何進行反轉呢?

例題:給定一個16位有符號整數,要求將其反轉後輸出(eg:輸入:1234,輸出:4321)。來看代碼:

func reverse(x:Int) -> Int {
    var i = 0
    var t = x
    //支持正數 負數 
    while t != 0 {
        i = 10*i + t%10
        t = t/10
    }
    if i < INT16_MIN || i > INT16_MAX {
        //超出 16位符號整型 輸出0
        return 0
    }
    return i
}
複製代碼

這道題也是LeetCode比較基礎的算法題:整數反轉。很是簡單,可是必定要注意邊界條件的判斷。

3 鏈表

筆者理解的鏈表是一串在存儲空間上非連續性、順序的存儲結構.由節點進行頭尾依次鏈接而造成鏈表.每一個結點有包括兩個部分:數據域和下個節點的指針域.

接下來看下書中在swift裏如何實現一個鏈表

1:生成節點

class ListNode {
    //數據域
    var val: Int
    //指針域
    var next: ListNode?
    
    init(_ val: Int) {
        self.val = val
        self.next = nil
    }     
}
複製代碼

2:實現頭插法和尾插法 頭插法:當前節點插到第一個節點以前, 尾插法:當前節點插入到鏈表最後一個節點以後。

class List {
    
    var head: ListNode?
    var tail: ListNode?
    
    //頭插法
    func appendToHead(_ val: Int) {
        
        if head == nil {
           //建立頭節點 
            head = ListNode(val)
            tail = head
        } else {
            let temp = ListNode(val)
           //把當前head地址賦給temp的指針域
            temp.next = head
            head = temp
        }
        
    }
    
    //尾插法實現同上
    func appendToTail(_ val: Int) {
        if tail == nil {
            tail = ListNode(val)
            head = tail
        } else {
            tail!.next = ListNode(val)
            tail = tail!.next
        }
    }
}
複製代碼

例題: 1 ->3 ->5 ->2 ->4 ->2,當給定x=3時,要求返回 1 ->2 ->2 ->3 ->5 ->4. 書上給出的解題思路,將完整的鏈表經過條件判斷(入參x)分割成2個新的鏈表,而後再將2個新鏈表拼接成完整的鏈表,看下代碼.

func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
    //建立2個Dummy節點
    let prevDummy = ListNode(0), postDummy = ListNode(0)
    var prev = prevDummy, post = postDummy
    
    var node = head
    //判斷是否存在node
    while node != nil {
        //判斷數據是否小於x
        if node!.val < x {
            //小於x prev.next指針域指向node
            prev.next = node
            prev = node!
        } else
        {
            //同上 大於x 構建新的鏈表
            post.next = node
            post = node!
        }
        //判斷下個節點
        node = node!.next
    }
    //將尾節點next置爲nil,防止構成環
    post.next = nil
    //左右拼接(左邊尾節點指向右邊頭節點)
    prev.next = postDummy.next

    return prevDummy.next
}
複製代碼

關於Dummy節點,書中給出了詳細介紹.

上面代碼引入了Dummy節點,它的做用就是做爲一個虛擬的頭前結點。咱們引入它的緣由是咱們不知道要返回的新鏈表的頭結點是哪個,它有多是原鏈表的第一個節點,可能在原鏈表的中間,也可能在最後,甚至可能不存在(nil)。而Dummy節點的引入能夠巧妙的涵蓋全部以上狀況,咱們能夠用dummy.next方便得返回最終須要的頭結點。

註解:

1:首先建立左右兩個Dummy節點,而後經過while判斷node是否存在.

2:若存在再判斷節點的數據val和判斷條件x的關係,小於x則被鏈道左邊鏈表上,大於x被鏈到右邊鏈表.

3:執行完while,此時咱們已經拿到左右兩個新的鏈表.最後把左邊的尾節點指向右邊的頭節點就完成了完整鏈表的拼接.

注意:須要將右鏈表的尾節點置nil,防止構成環. 關於檢測鏈表中是否有環,能夠經過快行指針來檢測,若快指針和慢指針變成相等的了,就表明該鏈表有環,具體的就不在這裏介紹了,比較簡單.

4 棧和隊列

先來了解下棧和隊列的基本概念 棧:先進後出(First In Last Out)的結構,又叫FILO.能夠理解爲咱們小時候完的疊羅漢,最下面的人是第一躺下而最後一個起來的. 隊列:先進先出(First In First Out)的結構,又叫(FIFO).這個字面上理解就行,就像排隊買票同樣,先來的人先買到票.

瞭解了棧和堆的概念,來看下Swift怎麼來寫出個棧和隊列.

protocol Stack {
   //associatedtype:關聯類型 至關於範型 在調用的時候能夠根據associatedtype指定的Element來設置類型
   associatedtype Element
   //判斷棧是否爲空
   var isEmpty: Bool {get}
   //棧的大小
   var size: Int {get}
   //棧頂元素
   var peek: Element? {get}
  //mutating:當須要在協議(結構體,枚舉)的實例方法中,修改協議(結構體,枚舉)的實例屬性,須要用到mutating對實例方法進行修飾,否則會報錯.
  //進棧
  mutating func push(_ newElement: Element)
  //出棧
  mutating func pop() -> Element?
}
複製代碼

實現協議方法

struct IntegerStack: Stack {
//typealias:類型別名 指定協議關聯類型的具體類型 和associatedtype成對出現的
typealias Element  = Int

var isEmpty: Bool {return stack.isEmpty}

var size: Int {return stack.count}
//取出stack棧頂元素
var peek: Element? {return stack.last}

private var stack = [Element]()
//push 加入stack數組中
mutating func push(_ newElement: Element) {
   return stack.append(newElement)
}
//pop 刪除stack中最後一個元素
mutating func pop() -> Element? {
   return stack.popLast()
  }
}
複製代碼

註解:利用協議申明瞭棧的屬性和方法,並在結構體中聲明數組stack,對數組數據進行append和poplLast操做,完成入棧出棧操做,比較簡單.

再來看下隊列的實現:

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: Element? {return left.isEmpty ? right.first : left.last}

  //聲明左右2個數組
  private var left = [Element]()
  private var right = [Element]()

 //在right數組中添加新元素
  mutating func enqueue(_ newElement: Int) {
      right.append(newElement)
  }

 //出隊列時 首先判斷left是否爲空 
  mutating func dequeue() -> Element? {
      if left.isEmpty {
       //reversed: 倒序遍歷數組
          left = right.reversed()
       //刪除right數組
          right.removeAll()
      }
      //刪除left數組的最後一個元素
      return left.popLast()
  }
}
複製代碼

註解:上面代碼裏面分別用兩個數組分別完成入隊列和出隊列的操做,用right存儲入隊列的元素.當出隊列時會先判斷left是否爲空,若是爲空left數組指向倒序後的right數組,這時執行popLast,即實現了出隊列的操做.size peek isEmpty屬性也是充分經過左右兩個數組來進行判斷. 關於書上用left right兩個數組來造成一個隊列的寫法筆者本身也不是很清楚,但願有知道的同窗能夠給小弟指點一下.

以後書上提到了棧和隊列互相轉換,這塊部分由於篇幅緣由就不介紹了,不過大體思路是經過兩個棧/隊列,配合生成所對應的結構,是一種用空間複雜度換時間複雜度的寫法.有興趣的同窗能夠購買原書細讀.

例題:給出路徑 /a/./b/../b/c/,簡化成/c 首先分析一下這道題,輸入字符串x,輸出字符串y.而後咱們經過**'/'將輸入的字符串進行分割,分割成字符串數組,而後遍歷數組中字符的具體狀況("."表示當前目錄,"..**"表示上一級目錄)就能夠了,看下代碼.

func simplifyPath(path: String) -> String {
    //初始化存儲路徑的數組
    var pathStack = [String]()
    //經過"/"分割入參path
    let paths = path.components(separatedBy: "/")
    //遍歷paths
    for path in paths {
       // 當path爲"."時,表示當前目錄,因此不作處理
        guard path != "." else {
           //跳過循環
            continue
        }
        //當path等於".."時,表示返回上級目錄
        if path == ".." {
            //這裏邊界狀況必定要考慮,這也是個容易出錯的地方
            if (pathStack.count > 0){
               //pathStack有數據,刪除last元素
                pathStack.removeLast()
            }
        } else if path != "" {
           //判斷path不等於""的邊界狀況,添加路徑
            pathStack.append(path)
        }
    }
    //將pathStack進行遍歷拼接,得到最終結果res
    let res = pathStack.reduce(""){
        total, dir in "\(total)/\(dir)"
    }
    //若是是空路徑 返回 "/"
    return res.isEmpty ? "/" : res
}
複製代碼

上面的例題並非很難,可是想完整寫好卻不是十分容易.須要注意的邊界條件很是的多,若是在面試的時候只是順着解決問題的方向走而忽視了這些邊界問題,那麼給面試官的印象也會大打折扣.

5 二叉樹

概念:二叉樹中,每一個節點最多有兩個子節點,通常稱爲左子節點和右子節點,而且節點的順序不能任意顛倒.第一層的節點稱之爲根節點,咱們從根節點開始計算,到節點的最大層次爲二叉樹的深度. 首先看下書中節點實現的代碼:

//public:不一樣於OC的public swift中不能在override和繼承的extension中訪問
public class TreeNode {
    public var val: Int
    public var left: TreeNode?
    public var right: TreeNode?
    public init(_ val: Int){
        self.val = val
    }
}
複製代碼

一個完整的二叉樹節點包含數據val,左子節點和右子節點.

有了節點咱們再看下如何求二叉樹的深度

func treeMaxDepth(root: TreeNode?) -> Int {
    
    guard let root = root else {
        return 0;//root不存在返回0
    }
    //max函數:比較兩個入參的大小取最大值
    return max(treeMaxDepth(root:root.left), treeMaxDepth(root: root.right)) + 1 
}
複製代碼

註解:

1:首先判斷入參二叉樹節點是否爲nil,若不存在的話返回0,若存在遞歸子節點取最大值.

2:+1表示每遞歸一層,二叉樹深度+1.

3:max函數做用:左右子節點最大深度比較,取較大值.

二叉查找樹

在二叉樹中有類特別的二叉樹叫作二叉查找樹(Binary Sort Tree),知足全部左子節點的值都小於它的根節點的值,全部右字節點都大於它的根子節點的值,那麼問題來了咱們怎麼判斷一顆二叉樹是否爲二叉查找樹呢.來看代碼:

func isValidBST(root: TreeNode?) -> Bool {
    return _helper(node: root, nil, nil)
}

private func _helper(node: TreeNode?, _ min: Int?, _ max: Int?) -> Bool {
    //node不存在 則到了最底層 遞歸結束.這個要注意的是第一次傳入的node不能爲nil
    guard let node = node else {
        return true
    }
    //右子樹的值都必須大於它的根節點值
    if let min = min, node.val <= min {
        return false
    }
    //左子樹的值都必須小於它的根節點值
    if let max = max, node.val >= max {
        return false
    }
    //左右子樹同時遞歸
    return _helper(node: node.left, min, node.val) && _helper(node: node.right, node.val, max)
}
複製代碼

註解:

1:這個地方會首先判斷當前節點是否存在,若不存在即表明已經遞歸到最後一層,返回true.

2:而後判斷右子樹和左子樹的val是否知足判斷條件,若都知足條件進行下一層的遞歸.

3:左右同時進行遞歸操做.

知道了二叉樹的基本概念和swift的寫法外,再來看下如何實現二叉樹的遍歷操做.常見的二叉樹遍歷有前序、中序、後序和層級遍歷(廣度優先遍歷).

給出幾種遍歷方法的規則:

前序遍歷規則: 1:訪問根節點 2:前序遍歷左子樹 3:前序遍歷右子樹

中序遍歷規則: 1:中序遍歷左子樹 2:訪問根節點 3:中序遍歷右子樹

後序遍歷規則: 1:後序遍歷左子樹 2:後序遍歷右子樹 3:訪問根節點

**層級遍歷規則:**以層爲順序,將某一層上的節點所有遍歷完成後,才向下一層進行遍歷,依次類推,至到最後一層.

若是仍是不理解能夠看下這篇關於二叉樹遍歷的介紹,講的比較直白易懂.

關於遍歷部份內容由於比較多,筆者摘錄書上隊列實現樹來介紹下實現邏輯,看如下代碼:

func levelOrder(root: TreeNode?) -> [[Int]] {
    //初始化 遍歷後返回的res數組
    var res = [[Int]]()
    //隊列數組
    var queue = [TreeNode]()
    
    if let root = root {
        //存儲入參root節點
        queue.append(root)
    }
    
    while queue.count > 0 {
        //獲取層級個數
        let size = queue.count
        //每一層 數據存儲數組
        var level = [Int]()
        
        for _ in 0 ..< size {
            //removeFirst:刪除第一個元素 並返回被刪除的元素
            let node = queue.removeFirst()
            //添加數據
            level.append(node.val)
            //判斷是否存在 左右子節點 並添加到隊列 順序從左至右
            if let left = node.left {
                queue.append(left)
            }
            if let right = node.right {
                queue.append(right)
            }  
        }
        //存儲level數據數組
        res.append(level)
    }
    return res
}
複製代碼

註解:

1:首先咱們聲明數組res,用來存儲每一層的val數組,並聲明隊列數組,方便後面判斷.

2:判斷入參root是否存在,若存在存儲root節點到queue數組中.

3:while判斷queue是否有數據,如有數據則取到queue的第一個數據,並在queue中刪除.而後判斷下一層的數據,並再次循環遍歷.

4:最後把節點的val數據保存到level中,添加到res.

總結

關於本書中算法部分的基本數據結構就介紹的差很少了,後面會對具體的算法實戰部分進行介紹.筆者整體感受這本書更適合初級->中級的同窗,對於想要提升本身的初中級開發人員這本書絕對是不錯的選擇.

相關文章
相關標籤/搜索