Swift 算法實戰之路:排序


以前的文章中,我們主要是在講數據結構:比如數組、鏈表、隊列、樹。這些數據結構都是了解Swift和算法的基礎。從今以后的文章,我們將更多的關注于通用算法,這次我們就來聊聊排序。這次的主要內容有:

  • 基本概念
  • 排序實戰

基本概念

我們平常用的排序算法一般就以下幾種:

名稱 時間復雜度 空間復雜度 是否穩定
冒泡排序 O(n^2) O(1)
插入排序 O(n^2) O(1)
選擇排序 O(n^2) O(1)
堆排序 O(nlogn) O(1)
歸并排序 O(nlogn) O(n)
快速排序 O(nlogn) O(1)
桶排序 O(n) O(k)

這些算法具體的定義本文不再贅述。一般情況下,好的排序算法性能是O(nlogn),壞的性能是O(n^2)。本文在此用swift示范實現歸并排序:

func mergeSort(array: [Int]) -> [Int] {
  var helper = Array(count: array.count, repeatedValue: 0)
  var array = array
  mergeSort(&array, &helper, 0, array.count - 1)
  return array
}

func mergeSort(inout array: [Int], inout _ helper: [Int], _ low: Int, _ high: Int) {
  guard low < high else {
    return
  }
  
  let middle = (high - low) / 2 + low
  mergeSort(&array, &helper, low, middle)
  mergeSort(&array, &helper, middle + 1, high)
  merge(&array, &helper, low, middle, high)
}

func merge(inout array: [Int], inout _ helper: [Int], _ low: Int, _ middle: Int, _ high: Int) {
  // copy both halves into a helper array
  for i in low ... high {
    helper[i] = array[i]
  }
  
  var helperLeft = low
  var helperRight = middle + 1
  var current = low
  
  // iterate through helper array and copy the right one to original array
  while helperLeft <= middle && helperRight <= high {
    if helper[helperLeft] <= helper[helperRight] {
      array[current] = helper[helperLeft]
      helperLeft += 1
    } else {
      array[current] = helper[helperRight]
      helperRight += 1
    }
    current += 1
  }
  
  // handle the rest
  guard middle - helperLeft >= 0 else {
    return
  }
  for i in 0 ... middle - helperLeft {
    array[current + i] = helper[helperLeft + i]
  }
}

表格中有一個特例是桶排序,它是將輸入的數組分配到一定數量的空桶中,每個空桶再單獨排序。當輸入的數組是均勻分配時,桶排序的時間復雜度為O(n)。舉個微軟的面試題來當例子:

有三種顏色(紅,黃,藍)的球若干,要求將所有紅色的球放在黃色球的前面,最后放上所有的藍色球。

這道題目最直接的解法就是桶排序。首先第一次遍歷,統計有多少個紅色球(假設x個),多少個黃色球(假設y個),和多少個藍色球(假設z個)。然后再一次遍歷,數組前部x個位置填充紅色球,中間y個位置放上對應數量的黃色球,最后z個位置再放上藍色球。

另外解釋一下穩定的意思:相等的鍵值,如果排過序后與原來未排序的次序相同,則稱此排序算法為穩定。舉個例子:

// 原數組
[[2, 1], [1,3], [1,4]]

// 排序算法一
[[1,3], [1,4], [2, 1]]
// 排序算法二
[[1,4], [1,3], [2, 1]]

我們注意到排序算法一和二的區別就在于對[1, 3], [1, 4]這兩個元素的處理。排序算法一中,這兩個元素位置與原數組相同,故稱其為穩定算法。而排序算法二則是不穩定算法。

Swift中,排序的使用如下:

// 以升序排列為例,原數組可改變
array.sort

// 以降序排列為例,原數組不可改變
newArray = array.sorted(by: >)

// 字典鍵值排序示例
let keys = Array(map.keys)
let sortedKeys = keys.sorted() {
  return map[$0]! > map[$1]!
}

在其他語言比如Java中,其自帶的sort函數是用歸并排序實現的。而在Swift源代碼中,sort函數采用的是一種內審算法(IntroSort)。它由堆排序、插入排序、快速排序三種算法構成,依據輸入的深度相應選擇最佳的算法來完成。本文關注的重點是實戰,所以不做展開。對源代碼感興趣的朋友可以去Github讀蘋果的Swift的開源庫。

排序實戰

直接來看一道Facebook, Google, Linkedin都考過的面試題。

已知有很多會議,如果有這些會議時間有重疊,則將它們合并。
比如有一個會議的時間為3點到5點,另一個會議時間為4點到6點,那么合并之后的會議時間為3點到6點

解決算法題目第一步永遠是把具體問題抽象化。這里每一個會議我們已知開始時間和結束時間,就可以寫一個類來定義它:

public class MeetingTime {
  public var start: Int
  public var end: Int
  public init(_ start: Int, _ end: Int) {
    self.start = start
    self.end = end
  }
}

然后題目說已知有很多會議,就是說我們已知有一個MeetingTime的數組、所以題目就轉化為寫一個函數,輸入為一個MeetingTime的數組,輸出為一個將原數組中所有重疊時間都處理過的新數組。

func merge(meetingTimes: [MeetingTime]) -> [MeetingTime] {}

下面來分析一下題目怎么解。最基本的思路是遍歷一次數組,然后歸并所有重疊時間。舉個例子:[[1, 3], [5, 6], [4, 7], [2, 3]]。這里我們可以發現[1, 3]和[2, 3]可以歸并為[1, 3],[5, 6]和[4, 7]可以歸并為[5, 7]。所以這里就提出一個要求:要將所有可能重疊的時間盡量放在一起,這樣遍歷的時候可以就可以從前往后一個接著一個的歸并。于是很自然的想到 -- 按照會議開始的時間排序。

這里我們要對一個class進行排序,而且要自定義排序方法,在Swift中可以這樣寫:

meetingTimes.sortInPlace() {
  if $0.start != $1.start {
    return $0.start < $1.start
  } else {
    return $0.end < $1.end
  }
}

意思就是首先對開始時間進行升序排列,如果它們相同,就比較結束時間。

有了排好順序的數組,要得到新的歸并后的結果數組,我們只需要在遍歷的時候,每次比較原數組(排序后)當前會議時間與結果數組中當前的會議時間,假如它們有重疊,則歸并;如果沒有,則直接添加進結果數組之中。所有代碼如下:

func merge(meetingTimes: [MeetingTime]) -> [MeetingTime] {
  // 處理特殊情況
  guard meetingTimes.count > 1 else {
    return meetingTimes
  }

  // 排序  
  var meetingTimes = meetingTimes.sort() {
    if $0.start != $1.start {
      return $0.start < $1.start
    } else {
      return $0.end < $1.end
    }
  }

  // 新建結果數組
  var res = [MeetingTime]()
  res.append(meetingTimes[0])

  // 遍歷排序后的原數組,并與結果數組歸并     
  for i in 1..<meetingTimes.count {
    let last = res[res.count - 1]
    let current = meetingTimes[i]
    if current.start > last.end {
      res.append(current)
    } else {
      last.end = max(last.end, current.end)
    }
  }
        
  return res
}

展望

排序在Swift中的應用場景很多,比如tableView中對于dataSource的處理。當然很多時候,排序都是和搜索,尤其是二分搜索配合使用。下期探討搜索的時候,會對排序進行進一步拓展。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容