IT名企演算法與資料結構題目最優解--棧和佇列

YDLIN發表於2018-07-05

前言

最近在看演算法相關的書,無意中看到了這本《程式設計師程式碼面試指南:IT名企演算法與資料結構題目最優解》

IT名企演算法與資料結構題目最優解--棧和佇列
由於書中的程式碼使用Java實現的,所以我這裡用swift去實現了一遍,即當作鍛鍊,也為了以後複習方便點(畢竟我看swift比看Java容易)。還有就是這裡我只貼了該書的題目,並沒有貼詳細的推導過程,雖然程式碼裡面有部分的註釋,但是還是希望大家能買書去參考。廢話不多說,直接看題.

設計一個有getMin功能的棧

  • 【題目】
    • 實現一個特殊的棧,在實現棧的基本功能的基礎上,再實現返回棧中最小元素的操作。
  • 【解題】
    • 由於Swift中沒有提供現成的棧這種資料結構,所以我們先實現一個基礎的棧,然後再實現一個能返回一個最小元素的棧。我們在這裡通過協議來實現,首先我們定義兩個協議:
//這裡我們只關注幾個基礎操作就行了
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?
}

//這裡是獲取最小值
protocol getMin {
    associatedtype Element
    mutating func min() -> Element?
}
複製程式碼

用這兩個協議來實現一個存放Integer的棧:

struct IntStack: Stack {
    typealias Element = Int
    
    private var stack = [Element]()
    
    var isEmpty: Bool {
        return stack.isEmpty
    }
    
    var size: Int {
        return stack.count
    }
    
    var peek: Int? {
        return stack.last
    }
    
    mutating func push(_ newElement: Int) {
        stack.append(newElement)
    }
    
    mutating func pop() -> Int? {
        return stack.popLast()
    }
}
複製程式碼

進行到這一步的時候,我們已經擁有一個具備幾個基礎操作的棧了,然後我們使用這種棧來實現一個可以返回最小值的特殊棧:

struct getMinStack: Stack, getMin {
    typealias Element = Int
    
    private var stackData = IntStack()
    private var stackMin = IntStack()
    
    var isEmpty: Bool {
        return stackData.isEmpty
    }
    
    var size: Int {
        return stackData.size
    }
    
    var peek: Int? {
        return stackData.peek
    }
    
    mutating func push(_ newElement: Int) {
        //stackData
        stackData.push(newElement)
        //stackMin
        if stackMin.isEmpty {
            stackMin.push(newElement)
        }else {
            if let minObject = min() {
                if newElement <= minObject {
                    stackMin.push(newElement)
                }
            }
        }
    }
    
    mutating func pop() -> Int? {
        return stackData.pop()
    }
    
    func min() -> getMinStack.Element? {
        if !stackMin.isEmpty {
            return stackMin.peek
        }else {
            return nil
        }
    }
}
複製程式碼

現在我們來測試一下:

let testArray = [3, 4, 5, 7, 6, 9, 2, 10]
let minStack = getMinStack()
for num in testArray {
    minStack.push(num)
}
if !minStack.isEmpty {
    debugPrint(minStack.min()!)// 輸出2
}
複製程式碼

由兩個棧組成的佇列

  • 【題目】
    • 編寫一個類,用兩個棧實現佇列,支援佇列的基本操作(add、poll、peek)。
  • 【解答】
    • 基於上題的程式碼,我們這裡再定義一個Queue協議
protocol Queue {
    associatedtype Element
    mutating func add(_ newElement: Element)
    mutating func poll() -> Element?
    mutating func peek() -> Element?
}
複製程式碼

實現一個QueueFromStack結構體:

struct QueueFromStack: Queue {
    typealias Element = Int
    var stackPush = IntStack()
    var stackPop = IntStack()
    
    mutating func add(_ newElement: Int) {
        stackPush.push(newElement)
    }
    
    mutating func poll() -> Int? {
        if stackPush.isEmpty && stackPop.isEmpty {
            debugPrint("Queue is empty!")
            return nil
        }else if stackPop.isEmpty {
            while !stackPush.isEmpty {
                stackPop.push(stackPush.pop()!)
            }
        }
        return stackPop.pop()
    }
    
    mutating func peek() -> Int? {
        if stackPush.isEmpty && stackPop.isEmpty {
            debugPrint("Queue is empty!")
            return nil
        }else if stackPop.isEmpty {
            while !stackPush.isEmpty {
                stackPop.push(stackPush.pop()!)
            }
        }
        return stackPop.peek
    }
}
複製程式碼

現在我們來測試一下:

let testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9]
var queue = QueueFromStack()
for num in testArray {
	queue.add(num)
}
        
debugPrint(queue.peek()!)// 輸出1
debugPrint(queue.poll()!)// 輸出1
debugPrint(queue.peek()!)// 輸出2
複製程式碼

僅用遞迴函式和棧操作逆序一個棧

  • 【題目】
    • 一個棧依次壓入1、2、3、4、5,那麼從棧頂到棧底分別為5、4、3、2、1。將這個棧轉置後,從棧頂到棧底為1、2、3、4、5,也就是實現棧中元素的逆序,但是隻能用遞迴函式來實現,不能用其他資料結構。
  • 【解題】
    • 先實現一個移除棧底元素,並返回棧底元素的函式:
func getAndRemoveLastElement(stack: inout IntStack) -> Int {
    guard let result = stack.pop() else { return -1}
    if stack.isEmpty {
        return result
    }else {
        let last = getAndRemoveLastElement(stack: &stack)
        stack.push(result)//此時stack已經是移除棧底元素的stack
        return last
    }
}
複製程式碼

接下來對棧進行逆序操作,需要用到上面的遞迴函式:

func reverse(stack: inout IntStack) {
    if stack.isEmpty {
        //經過getAndRemoveLastElement(stack:)後,每次都會移除棧底元素,最終棧中元素會清空,就會來到這裡
        return
    }
    /*
     多次返回棧底元素
     1->2->3
    */
    let i = getAndRemoveLastElement(stack: &stack)
    reverse(stack: &stack)
    /*
     此時stack已經為空,然後一次逆序push上一個棧返回的棧底元素
     push i = 3 -> [3]
     push i = 2 -> [3, 2]
     push i = 3 -> [3, 2, 1]
     */
    stack.push(i)
}
複製程式碼

現在我們來測試一下:

let testArray = [1, 2, 3]
var stack = IntStack()
for num in testArray {
    stack.push(num)
}
reverse(stack: &stack)
print(stack)//[3, 2, 1]
複製程式碼

貓狗佇列

  • 【題目】
    • 寵物、狗和貓的類如下:
class Pet {
    private var type: String?
    init(type: String) {
        self.type = type
    }
    
    public func getType() -> String? {
        return type
    }
}

class Dog: Pet {
    init() {
        super.init(type: "Dog")
    }
}

class Cat: Pet {
    init() {
        super.init(type: "Cat")
    }
}
複製程式碼

實現一種狗貓佇列的結構,要求如下:

  • 使用者可以呼叫add方法將cat類或dog類的例項放入佇列中;
  • 使用者可以呼叫pollAll方法,將佇列中所有的例項按照進佇列的先後順序依次彈出;
  • 使用者可以呼叫pollDog方法,將佇列中dog類的例項按照進佇列的先後順序依次彈出;
  • 使用者可以呼叫pollCat方法,將佇列中cat類的例項按照進佇列的先後順序依次彈出;
  • 使用者可以呼叫isEmpty方法,檢查佇列中是否還有dogcat的例項;
  • 使用者可以呼叫isDogEmpty方法,檢查佇列中是否有dog類的例項;
  • 使用者可以呼叫isCatEmpty方法,檢查佇列中是否有cat類的例項。
  • 【解題】
class PetEnterQueue {
    private var pet: Pet?
    private var count: Int?
    init(pet: Pet, count: Int) {
        self.pet = pet
        self.count = count
    }
    
    public func getPet() -> Pet? {
        return self.pet
    }
    
    public func getCount() -> Int? {
        return self.count
    }
    
    public func getEnterPetType() -> String? {
        return self.pet?.getType()
    }
}

class DogCatQueue {
    private var dogQ: LinkedList<PetEnterQueue>!
    private var catQ: LinkedList<PetEnterQueue>!
    private var count = 0
    
    init() {
        dogQ = LinkedList<PetEnterQueue>()
        catQ = LinkedList<PetEnterQueue>()
    }
    
    public func add(pet: Pet) {
        let timeInterval: TimeInterval = Date().timeIntervalSince1970
        let timeStamp = Int(timeInterval)
        if pet.getType() == "Dog" {
            dogQ.appendToTail(value: PetEnterQueue(pet: pet, count: timeStamp))
        }else if pet.getType() == "Cat" {
            catQ.appendToTail(value: PetEnterQueue(pet: pet, count: timeStamp))
        }else {
            fatalError("error, not dog or cat")
        }
    }
    
    public func pollAll() -> Pet? {
        if !dogQ.isEmpty && !catQ.isEmpty {
            let dog = dogQ.last?.value
            let cat = catQ.last?.value
            if (dog?.getCount())! < (cat?.getCount())! {
                return dogQ?.last?.value.getPet()
            }else {
                return catQ?.last?.value.getPet()
            }
        }else if !dogQ.isEmpty {
            return dogQ.last?.value.getPet()
        }else if !catQ.isEmpty {
            return catQ?.last?.value.getPet()
        }else {
            fatalError("error, queue is empty!")
        }
    }
    
    public func pollDog() -> Dog {
        if !isDogQueueEmpty() {
            return dogQ.first?.value.getPet() as! Dog
        }else {
            fatalError("Dog queue is empty!")
        }
    }

    public func pollCat() -> Cat {
        if !isCatQueueEmpty() {
            return catQ.first?.value.getPet() as! Cat
        }else {
            fatalError("Cat queue is empty!")
        }
    }
    
    public func isEmpty() -> Bool {
        return dogQ.isEmpty && catQ.isEmpty
    }
    
    public func isDogQueueEmpty() -> Bool {
        return dogQ.isEmpty
    }
    
    public func isCatQueueEmpty() -> Bool {
        return catQ.isEmpty
    }
}	
複製程式碼

用一個棧實現另一個棧的排序

  • 【題目】
    • 一個棧中元素的型別為整型,現在想將該棧從頂到底按從大到小的順序排序,只許申請一個棧。除此之外,可以申請新的變數,但不能申請額外的資料結構。如何完成排序?
  • 【解題】
func sortStackByStack(stack: IntStack) -> IntStack {
    var tempStack = stack
    //申請的輔助棧
    var helpStack = IntStack()
    while !tempStack.isEmpty {//如果原來棧的元素全都push到輔助棧上,則證明輔助棧從棧頂到棧底已經按小到大排好序了,則停止迴圈
        guard let cur = tempStack.pop() else { return tempStack }
        if helpStack.isEmpty {//輔助棧為空
            helpStack.push(cur)
        }else {//輔助棧不為空
            while helpStack.peek! <= cur {
                if let topElement = helpStack.pop() {
                    tempStack.push(topElement)
                }
            }
            helpStack.push(cur)
        }
    }
    //將輔助棧的元素逐一pop出來,push回到原來的棧上(原來棧已空),則原來棧就從棧頂到棧底就是按照從大到小的排序
    while !helpStack.isEmpty {
        if let element = helpStack.pop() {
            tempStack.push(element)
        }
    }
    return tempStack
}
複製程式碼

現在我們來測試一下:

var stack = IntStack()
let testArray = [3, 4, 5, 7, 6, 9, 2, 10]
for num in testArray {
    stack.push(num)
}
print(sortStackByStack(stack: stack))//IntStack(stack: [2, 3, 4, 5, 6, 7, 9, 10])
複製程式碼

用棧來求解漢諾塔問題

  • 【題目】
    • 漢諾塔問題比較經典,這裡修改一下游戲規則:現在限制不能從最左側的塔直接移動到最右側,也不能從最右側直接移動到最左側,而是必須經過中間。求當塔有N層的時候,列印最優移動過程和最優移動總步數。例如,當塔數為兩層時,最上層的塔記為1,最下層的塔記為2,則列印:

    Move 1 from left to mid

    Move 1 from mid to right

    Move 2 from left to mid

    Move 1 from right to mid

    Move 1 from mid to left

    Move 2 from mid to right

    Move 1 from left to mid

    Move 1 from mid to right

    It will move 8 steps.

  • 【要求】
    • 用以下兩種方法解決。
    • 方法一:遞迴的方法;
    • 方法二:非遞迴的方法,用棧來模擬漢諾塔的三個塔。
  • 【解答一】:使用遞迴
func hanoiProblem(num: Int, left: String, mid: String, right: String) -> Int {
    if num < 1 {
        return 0
    }
    //這裡是從左邊挪到右邊	
    return process(num, left, mid, right, from: left, to: right)
}

func process(_ num: Int, _ left: String, _ mid: String, _ right: String, from: String, to: String) -> Int {
    if num == 1 {//一層
        if from == mid || to == mid {//中->左、中->右、左->中、右->中
            print("Move 1 from \(from) to \(to)")
            return 1
        }else {//左->右、右->左
            print("Move 1 from \(from) to \(mid)")
            print("Move 1 from \(mid) to \(to)")
            return 2
        }
    }
    
    //多層
    if from == mid || to == mid {//中->左、中->右、左->中、右->中
        let another = (from == left || to == left) ? right : left
        let part1 = process(num - 1, left, mid, right, from: from, to: another)
        let part2 = 1
        print("Move \(num) from \(from) to \(to)")
        let part3 = process(num - 1, left, mid, right, from: another, to: to)
        return part1 + part2 + part3
    }else {//左->右、右->左
        let part1 = process(num - 1, left, mid, right, from: from, to: to)
        let part2 = 1
        print("Move \(num) from \(from) to \(mid)")
        let part3 = process(num - 1, left, mid, right, from: to, to: from)
        let part4 = 1
        print("Move \(num) from \(mid) to \(to)")
        let part5 = process(num - 1, left, mid, right, from: from, to: to)
        return part1 + part2 + part3 + part4 + part5
    }
}
複製程式碼

現在我們來測試一下:

let num = hanoiProblem(num: 2, left: "左", mid: "中", right: "右")
print("It will move \(num) steps.")

/*列印結果
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 左 to 中
Move 1 from 右 to 中
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
It will move 8 steps.
*/
複製程式碼
  • 【解答二】:不使用遞迴,使用棧
//移動方式
public enum Action {
    case No  //初始狀態
    case LToM//左->中
    case MToL//中->左
    case MToR//中->右
    case RToM//右->中
}
/*
 相鄰不可逆原則:假設這一步是左->中,那麼下一步就不可能是中->左,因為這樣的話就不是移動次數最小的最優解
 小壓大原則:一個動作的放生的前提條件是移出棧的棧頂元素不能大於移入棧的棧頂元素
 */
func hanoiProblem2(num: Int, left: String, mid: String, right: String) -> Int {
    //左棧
    var lS = IntStack()
    //中間棧
    var mS = IntStack()
    //右棧
    var rS = IntStack()
    //先往每個棧底放一個最大的整數
    lS.push(Int.max)
    mS.push(Int.max)
    rS.push(Int.max)
    
    for i in (1...num).reversed() {
        lS.push(i)
    }
    
    var record = [Action.No]
    var step = 0
    while rS.size != num + 1 {//當右邊的棧等於(層數 + 1)就停止迴圈,表明所有的層都移到右邊的棧裡面去了
        /*
         1.第一個動作一定是:左->中
         2.根據”相鄰不可逆“和”小壓大“兩個原則,所以第二個動作不肯是中->左和左->中
         3.那剩下的只有中->右,右->中兩種移動方式,又根據”小壓大“原則,這兩種方式裡面,只有一種是符合要求的
         綜上,每一步只有一個方式符合,那麼每走一步都根據兩個原則來檢視所有方式,只需執行符合要求的方式即可
         */
        step += fStackTotStack(record: &record, preNoAct: .MToL, nowAct: .LToM, fStack: &lS, tStack: &mS, from: left, to: mid)
        step += fStackTotStack(record: &record, preNoAct: .LToM, nowAct: .MToL, fStack: &mS, tStack: &lS, from: mid, to: left)
        step += fStackTotStack(record: &record, preNoAct: .RToM, nowAct: .MToR, fStack: &mS, tStack: &rS, from: mid, to: right)
        step += fStackTotStack(record: &record, preNoAct: .MToR, nowAct: .RToM, fStack: &rS, tStack: &mS, from: right, to: mid)
    }
    return step
}

func fStackTotStack(record: inout [Action], preNoAct: Action, nowAct: Action, fStack: inout IntStack, tStack: inout IntStack, from: String, to: String) -> Int {
    guard let fTop = fStack.peek else { return 0 }
    guard let tTop = tStack.peek else { return 0 }
    if record[0] != preNoAct && fTop < tTop {//相鄰不可逆原則 && 小壓大原則
        if let topElement = fStack.pop() {
            tStack.push(topElement)
        }
        guard let tTop2 = tStack.peek else { return 0 }
        print("Move \(tTop2) from \(from) to \(to)")
        record[0] = nowAct
        return 1
    }
    return 0
}
複製程式碼

現在我們來測試一下:

let step = hanoiProblem2(num: 3, left: "左", mid: "中", right: "右")
print("It will move \(step) steps.")
/*列印結果
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 左 to 中
Move 1 from 右 to 中
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 3 from 左 to 中
Move 1 from 右 to 中
Move 1 from 中 to 左
Move 2 from 右 to 中
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 中 to 左
Move 1 from 右 to 中
Move 1 from 中 to 左
Move 3 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
Move 2 from 左 to 中
Move 1 from 右 to 中
Move 1 from 中 to 左
Move 2 from 中 to 右
Move 1 from 左 to 中
Move 1 from 中 to 右
It will move 26 steps.
*/
複製程式碼

生成視窗最大值陣列

  • 【題目】
    • 有一個整型陣列arr和一個大小為w的視窗從陣列的最左邊滑到最右邊,視窗每次向右邊滑一個位置。例如,陣列為[4,3,5,4,3,3,6,7],視窗大小為3時:

[4 3 5] 4 3 3 6 7 視窗中最大值為5

4 [3 5 4] 3 3 6 7 視窗中最大值為5

4 3 [5 4 3] 3 6 7 視窗中最大值為5

4 3 5 [4 3 3] 6 7 視窗中最大值為4

4 3 5 4 [3 3 6] 7 視窗中最大值為6

4 3 5 4 3 [3 6 7] 視窗中最大值為7

如果陣列長度為n,視窗大小為w,則一共產生n-w+1個視窗的最大值。請實現一個函式。

輸入:整型陣列arr,視窗大小為w。

輸出:一個長度為n-w+1的陣列res,res[i]表示每一種視窗狀態下的最大值。以本題為例,結果應該返回{5,5,5,4,6,7}。

  • 【解答】
func getMaxWindow(array: Array<Int>, windowSize w: Int) -> Array<Int> {
    if array.isEmpty || w < 1 || array.count < w {
        return []
    }
    
    /*
     1.用來存放array的下標
     2.通過不斷的更新,qmax裡面存放的第一個下標所對應array中的元素就是當前視窗子陣列的最大元素
     */
    var qmax = [Int]()
    //初始化一個長度為(array.count - w + 1)的陣列
    var res = Array.init(repeating: 0, count: (array.count - w + 1))
    var index: Int = 0
    
    for i in (0..<array.count) {
        /*************qmax的放入規則***************/
        //qmax不為空
        while !qmax.isEmpty && array[qmax.last!] <= array[i] {
            qmax.removeLast()
        }
        
        //如果qmax為空,直接把下標新增進去
        //如果array[qmax.last!] > array[i],直接把下標新增進去
        qmax.append(i)
        
        /*************qmax的彈出規則***************/
        //如果下標過期,則刪除最前面過期的元素
        if let firstIndex = qmax.first {
            if firstIndex == (i - w) {
                qmax.removeFirst()
            }
        }
        
        /*
         這個條件是確保第一個視窗中的每個元素都已經遍歷完
         這個例子中:第一個視窗對應的子陣列是[4, 3, 5],所以i要去到2的時候才知道5是子陣列裡面的最大值,這樣才能把最大值取出,放入res陣列中。之後每移動一步都會產生一個最大
         */
        if i >= w - 1 {
            if index <= res.count - 1 {//確保不越界
                if let fistIndex = qmax.first {
                    res[index] = array[fistIndex]
                }
                index += 1
            }
        }
    }
    return res
}
複製程式碼

現在我們來測試一下:

let array1 = [4, 3, 5, 4, 3, 3, 6, 7]
print(getMaxWindow(array: array1, windowSize: 3))
let array2 = [44, 31, 53, 14, 23, 93, 46, 27]
print(getMaxWindow(array: array2, windowSize: 4))
/*
列印結果為:
[5, 5, 5, 4, 6, 7]
[53, 53, 93, 93, 93]
*/
複製程式碼

最大值減去最小值小於或等於num的子陣列數量

  • 【題目】
    • 給定陣列arr和整數num,共返回有多少個子陣列滿足如下情況: max(arr[i..j])-min(arr[i..j])<=num max(arr[i..j])表示子陣列arr[i..j]中的最大值,min(arr[i..j])表示子陣列arr[i..j]中的最小值。
  • 【要求】
    • 如果陣列長度為N,請實現時間複雜度為O(N)的解法。
  • 【解答】
func getNum(array: Array<Int>, num: Int) -> Int {
    if array.isEmpty || array.count == 0 {
        return 0
    }
    //首元素代表當前子陣列中的最大值的下標
    var qmax = [Int]()
    //首元素代表當前子陣列中的最小值的下標
    var qmin = [Int]()
    
    //表示陣列的範圍
    var i = 0
    var j = 0
    //滿足條件的子陣列數量
    var res = 0
    
    while i < array.count {
        while j < array.count {
            while !qmin.isEmpty && array[qmin.last!] >= array[j] {
                qmin.removeLast()
            }
            qmin.append(j)
            while !qmax.isEmpty && array[qmax.last!] <= array[j] {
                qmax.removeLast()
            }
            qmax.append(j)
            //不滿足題目的要求,j就停止向右擴
            if array[qmax.first!] - array[qmin.first!] > num {
                break
            }
            //j向右擴
            j += 1
        }
        
        if let firstIndex = qmin.first {
            if firstIndex == i {
                qmin.removeFirst()
            }
        }
        
        if let firstIndex = qmax.first {
            if firstIndex == i {
                qmax.removeFirst()
            }
        }
        
        // 假設i = 0,j = 5時:[0..4], [0..3], [0..2], [0..1], [0..0], 一共(j - i)個符合條件的子陣列
        //j向右擴停止後,所以array[i..j-1]->array[i..i]子陣列都符合條件,所以res += j - i
        res += (j - i)
       
        i += 1
    }
    return res
}
複製程式碼

現在我們來測試一下:

let array = [3, 4, 5, 7]
print(getNum(array: array, num: 2))
/*
符合要求的陣列有:
[3]
[4]
[5]
[7]
[3, 4]
[4, 5]
[5, 7]
[3, 4, 5]
*/
複製程式碼

到此就是本文的結尾,如果大家覺得亂的話,可以直接通過Xcode來進行一步步除錯,這裡附上我寫的程式碼

後記

本篇只實現了第一章部分的題目,等以後有空再補回來。對於剩下的章節,會隨著我的看書進度,也會慢慢補上。如果你們喜歡本文的話,希望能打賞個贊,也當作是未來的動力。謝謝!!!!

相關文章