Swift算法-归并排序Merge Sort

声明:算法和数据结构的文章均是作者从github上翻译过来,为方便大家阅读。如果英语阅读能力强的朋友,可以直接到swift算法俱乐部查看所有原文,以便快速学习。作者同时也在学习中,欢迎交流

归并排序算法的目的:将一个数组按照从小到大或者从大到小排序

归并排序算法于1945年由John von Neumann发明,在各种情况下,它的运算性能均为O(nlogn)。

归并排序的主要核心思想是分而治之:将一个很大的问题分解为几个小的问题,然后将这些小问题解决,从而等同于解决了这个大问题。

假设你有一个含n个数的数组,你需要将它们按照一定顺序整理起来。归并排序算法将会按照以下过程处理:

1.将所有数字放在一起,没有任何规律,比如排成一个横排。
2.将这一横排的数据从中间分开,变成两个横排,均未整理。
3.继续重复步骤2,直到无法再拆分。
4.将分开的小横排按照顺序两两合并在一起,形成排序好的数组。每一次合并,都是得到排序好的新数组。

举例

拆分

有一个未整理的数组为[2, 1, 5, 4, 9]。我们将这个数组拆分到无法继续拆分为止。

首先,我们可以将该数组拆分为[2, 1][5, 4, 9]。这两个数组还可以继续拆分么?显而易见,可以的!

我们继续将[2, 1]拆分为[1][2]。该数组无法继续拆分。而[5, 4, 9]可以拆分为[5][4, 9],然后[4, 9]继续拆分为[4][9]

所以,在拆分结束时,我们得到[2][1][5][4][9]。其实,最后得到的每个数组都只含有一个数字。

合并

拆分完数组之后,我们需要将这些小数组进行有序合并。记住,我们的目的是将一个大的问题拆分成几个小问题来解决。每一次合并的时候,我们只会关心将要合并的两个数组,其它的数组暂不考虑。

拆分的时候,我们得到的数组是[2][1][5][4][9],在第一次合并的时候,我们会得到[1, 2][4, 5][9]。因为这里的[9]是最后一个数组,没有多余的数组可以在这一次合并的时候跟它合并,所以轮空。

第二次合并,我们得到[1, 2, 4, 5][9],由于[9]依然是最后一个数组,没有多余数组,继续轮空。

第三次合并,由于我们只剩下两个数组,我们得到[1, 2, 4, 5, 9],排序结束。

从上向下执行

在Swift中,归并排序算法代码如下:

func mergeSort(_ array: [Int]) -> [Int] {
  guard array.count > 1 else { return array }    // 1

  let middleIndex = array.count / 2              // 2

  let leftArray = mergeSort(Array(array[0..<middleIndex]))             // 3

  let rightArray = mergeSort(Array(array[middleIndex..<array.count]))  // 4

  return merge(leftPile: leftArray, rightPile: rightArray)             // 5
}

代码解析:
1.如果传入数组为空或者只有一个元素,我们就不需要继续拆分,直接返回该传入数组。
2.找出数组个数中间值。
3.使用步骤2得到的中间值,不断的拆分左边的数组。
4.同时不断拆分右边的数组。
5.最后,将所有数组合并到一起,得到排序数组。

合并过程代码如下:

func merge(leftPile: [Int], rightPile: [Int]) -> [Int] {
  // 1
  var leftIndex = 0
  var rightIndex = 0

  // 2 
  var orderedPile = [Int]()

  // 3
  while leftIndex < leftPile.count && rightIndex < rightPile.count {
    if leftPile[leftIndex] < rightPile[rightIndex] {
      orderedPile.append(leftPile[leftIndex])
      leftIndex += 1
    } else if leftPile[leftIndex] > rightPile[rightIndex] {
      orderedPile.append(rightPile[rightIndex])
      rightIndex += 1
    } else {
      orderedPile.append(leftPile[leftIndex])
      leftIndex += 1
      orderedPile.append(rightPile[rightIndex])
      rightIndex += 1
    }
  }

  // 4
  while leftIndex < leftPile.count {
    orderedPile.append(leftPile[leftIndex])
    leftIndex += 1
  }

  while rightIndex < rightPile.count {
    orderedPile.append(rightPile[rightIndex])
    rightIndex += 1
  }

  return orderedPile
}

上述代码看起来貌似很复杂,但其实很直接:

1.在合并过程中,你需要创建2个索引来追踪合并的两个数组。
2.这里的空数组为合并数组,在接下来的步骤里,这个空数组会不断地从其它数组放入新元素。
3.这里的while循环会将左右两侧的数组进行比较,并按照大小放入到orderedPile里。
4.如果执行到步骤4,则证明leftPilerightPile里的数字均已完全合并到orderedPile里。这时候,我们不需要继续做比较,只需要将剩下的数组合并起来。

这里我们举一个简单的例子来说明merge()的运行过程。假设我们有leftPile = [1, 7, 8]rightPile = [3, 6, 9]。注意这里每一个数组都是已经整理好顺序的,在归并排序算法里,这个是必要条件。我们按照下图步骤将这两个数组合并到一起:

leftPile       rightPile       orderedPile
[ 1, 7, 8 ]    [ 3, 6, 9 ]     [ ]
  l              r

左侧的索引l,右侧为r,当前左侧索引对应的数值为1,右侧索引对应的数值为3,因此,orderedPile添加进去的第一个数字为1。我们将l向右移动一个位置。

现在左侧索引对应的数字为7,右侧为3,因此,第二个放入的数字为3,右侧索引向右移动一个位置:

leftPile       rightPile       orderedPile
[ 1, 7, 8 ]    [ 3, 6, 9 ]     [ 1, 3 ]
     l           -->r

不断重复上述过程。在每一次过程中,我们都会从左侧或者右侧数组中拿出一个数字加入orderPile

leftPile       rightPile       orderedPile
[ 1, 7, 8 ]    [ 3, 6, 9 ]     [ 1, 3, 6 ]
     l              -->r

leftPile       rightPile       orderedPile
[ 1, 7, 8 ]    [ 3, 6, 9 ]     [ 1, 3, 6, 7 ]
     -->l              r

leftPile       rightPile       orderedPile
[ 1, 7, 8 ]    [ 3, 6, 9 ]     [ 1, 3, 6, 7, 8 ]
        -->l           r

现在左侧数组已经为空,我们只需要将右侧数组剩下的数值直接加入orderPile,排序完成。我们得到[1, 3, 6, 7, 8, 9]

这个算法其实很简单:在两个数组中,每次各取出一个数字进行比较,将最小的数字放入整理好的数组里,重复过程直到结束。当然,要注意前提是这两个数组都是排序好的数组。

从下向上执行

前一部分我们描述了归并算法里是由上向下的执行方法,因为该方法是先将数组拆分为小数组,然后再合并。其实,在整理过程中,我们可以跳过拆分过程,直接从合并数组开始。这种方法称作从下向上。

代码如下:

func mergeSortBottomUp<T>(_ a: [T], _ isOrderedBefore: (T, T) -> Bool) -> [T] {
  let arrayCount = a.count

  var arrayMatrix = [a, a]      // 1
  var sortedArrayIndex = 0

  var width = 1
  while width < arrayCount {   // 2

    var i = 0
    while i < arrayCount {     // 3

      var j = i
      var l = i
      var r = i + width

      let lmax = min(l + width, n)
      let rmax = min(r + width, n)

      while l < lmax && r < rmax {                // 4
        if isOrderedBefore(z[d][l], z[d][r]) {
          z[1 - d][j] = z[d][l]
          l += 1
        } else {
          z[1 - d][j] = z[d][r]
          r += 1
        }
        j += 1
      }
      while l < lmax {
        z[1 - d][j] = z[d][l]
        j += 1
        l += 1
      }
      while r < rmax {
        z[1 - d][j] = z[d][r]
        j += 1
        r += 1
      }

      i += width*2
    }

    width *= 2
    d = 1 - d      // 5
  }
  return z[d]
}

要点:

1.我们在合并左右两侧的数组时候,不能同时修改它们自身数组的内容,所以我们需要一个临时工作数组来执行这个功能。但是每一次合并过程都重新分配新的临时工作数组是非常浪费资源的,因此,我们使用两个工作数组来代替,并用d来区分它们,数组z[d]是用来读取,z[d-1]用来写,我们称之为双重缓冲。

2.从概念上来说,这一版本的工作方式和上一个版本的工作方式是相同的。首先,它会合并长度为1的数组,然后是2,然后是4,以此类推。数组的长度是有width决定的。最初的时候它的数值是1,然后在每个循环结束后乘以2。所以外循环决定了需要合并的数组长度,在每一次进入新的循环,需要合并的数组的长度跟着变得更大。

3.内循环是一个合并过程,合并后的数组会被放入z[1 - d]

4.从逻辑上来说,此版本与上一个版本相同,唯一不同的是,这里我们使用了双重缓冲,所以我们从z[d]上读数值,在z[1-d]写数组。这里的isOrderedBefore函数可以对比各种类型的数据,不仅仅是数字。

此函数为通用函数,我们可以对各种类型的数据进行排序。可以在playground进行测试:

let array = [2, 1, 5, 4, 9]
mergeSortBottomUp(array, <)   // [1, 2, 4, 5, 9]

性能分析

归类排序算法的速度取决于数组的大小。数组越大,它需要执行的时间越久。原始数组是否排序好与速度无关,因为无论是否排序,归类排序都需要先将数组拆分,然后再合并。因此,归类排序算法的时间复杂度永远为O(nlogn)

对于归类排序算法来说,它的缺点就是在排序过程中,需要创建一个大小与被排序数组相同的工作数组,这个与就地排序类型的算法不一样,比如说快速排序算法。

在绝大多数情况下,归类排序算法能得到一个稳定的有序数列。也就是说,在排序对比的依据相同的情况下,该序列的顺序是稳定的。这种特性对数字或者字符排序来说并无特别之处,但是在排序复杂对象的时候,效果显著。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,164评论 0 52
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,235评论 0 2
  • 概述排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的...
    Luc_阅读 2,253评论 0 35
  • 概述:排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    每天刷两次牙阅读 3,726评论 0 15
  • 看了微信公众号“大J小D”的一篇文章推送《为啥害怕女儿被夸漂亮?我们真正应该警惕的是这个》,深有同感呀!不吐不快,...
    打个盹儿发个呆阅读 530评论 0 8