【译】Swift算法俱乐部-第k大元素问题

本文是对 Swift Algorithm Club 翻译的一篇文章。
Swift Algorithm Clubraywenderlich.com网站出品的用Swift实现算法和数据结构的开源项目,目前在GitHub上有18000+⭐️,我初略统计了一下,大概有一百左右个的算法和数据结构,基本上常见的都包含了,是iOSer学习算法和数据结构不错的资源。
🐙andyRon/swift-algorithm-club-cn是我对Swift Algorithm Club,边学习边翻译的项目。由于能力有限,如发现错误或翻译不妥,请指正,欢迎pull request。也欢迎有兴趣、有时间的小伙伴一起参与翻译和学习🤓。当然也欢迎加⭐️,🤩🤩🤩🤨🤪。
本文的翻译原文和代码可以查看🐙swift-algorithm-club-cn/Kth Largest Element


第k大元素问题(k-th Largest Element Problem)

你有一个整数数组a。 编写一个算法,在数组中找到第k大的元素。

例如,第1个最大元素是数组中出现的最大值。 如果数组具有n个元素,则第n最大元素是最小值。 中位数是第n/2最大元素。

朴素的解决方案

以下是半朴素的解决方案。 它的时间复杂度是 O(nlogn),因为它首先对数组进行排序,因此也使用额外的 O(n) 空间。

func kthLargest(a: [Int], k: Int) -> Int? {
  let len = a.count
  if k > 0 && k <= len {
    let sorted = a.sorted()
    return sorted[len - k]
  } else {
    return nil
  }
}

kthLargest() 函数有两个参数:由整数组成的数组a,已经整数k。 它返回第k大元素。

让我们看一个例子并运行算法来看看它是如何工作的。 给定k = 4和数组:

[ 7, 92, 23, 9, -1, 0, 11, 6 ]

最初没有找到第k大元素的直接方法,但在对数组进行排序之后,它非常简单。 这是排完序的数组:

[ -1, 0, 6, 7, 9, 11, 23, 92 ]

现在,我们所要做的就是获取索引a.count - k的值:

a[a.count - k] = a[8 - 4] = a[4] = 9

当然,如果你正在寻找第k个最小的元素,你会使用a [k-1]

更快的解决方案

有一种聪明的算法结合了二分搜索快速排序的思想来达到O(n)解决方案。

回想一下,二分搜索会一次又一次地将数组分成两半,以便快速缩小您要搜索的值。 这也是我们在这里所做的。

快速排序还会拆分数组。它使用分区将所有较小的值移动到数组的左侧,将所有较大的值移动到右侧。在围绕某个基准进行分区之后,该基准值将已经处于其最终的排序位置。 我们可以在这里利用它。

以下是它的工作原理:我们选择一个随机基准,围绕该基准对数组进行分区,然后像二分搜索一样运行,只在左侧或右侧分区中继续。这一过程重复进行,直到我们找到一个恰好位于第k位置的基准。

让我们再看看初始的例子。 我们正在寻找这个数组中的第4大元素:

[ 7, 92, 23, 9, -1, 0, 11, 6 ]

如果我们寻找第k个最小项,那么算法会更容易理解,所以让我们采用k = 4并寻找第4个最小元素。

请注意,我们不必先对数组进行排序。 我们随机选择其中一个元素作为基准,假设是11,并围绕它分割数组。 我们最终会得到这样的结论:

[ 7, 9, -1, 0, 6, 11, 92, 23 ]
 <------ smaller    larger -->

如您所见,所有小于11的值都在左侧; 所有更大的值都在右边。基准值11现在处于最终排完序的位置。基准的索引是5,因此第4个最小元素肯定位于左侧分区中的某个位置。从现在开始我们可以忽略数组的其余部分:

[ 7, 9, -1, 0, 6, x, x, x ]

再次让我们选择一个随机的枢轴,让我们说6,然后围绕它划分数组。 我们最终会得到这样的结论:

[ -1, 0, 6, 9, 7, x, x, x ]

基准值6在索引2处结束,所以显然第4个最小的项必须在右侧分区中。 我们可以忽略左侧分区:

[ x, x, x, 9, 7, x, x, x ]

我们再次随机选择一个基准值,假设是9,并对数组进行分区:

[ x, x, x, 7, 9, x, x, x ]

基准值9的索引是4,这正是我们正在寻找的 k。 我们完成了! 注意这只需要几个步骤,我们不必先对数组进行排序。

以下函数实现了这些想法:

public func randomizedSelect<T: Comparable>(_ array: [T], order k: Int) -> T {
  var a = array

  func randomPivot<T: Comparable>(_ a: inout [T], _ low: Int, _ high: Int) -> T {
    let pivotIndex = random(min: low, max: high)
    a.swapAt(pivotIndex, high)
    return a[high]
  }

  func randomizedPartition<T: Comparable>(_ a: inout [T], _ low: Int, _ high: Int) -> Int {
    let pivot = randomPivot(&a, low, high)
    var i = low
    for j in low..<high {
      if a[j] <= pivot {
        a.swapAt(i, j)
        i += 1
      }
    }
    a.swapAt(i, high)
    return i
  }

  func randomizedSelect<T: Comparable>(_ a: inout [T], _ low: Int, _ high: Int, _ k: Int) -> T {
    if low < high {
      let p = randomizedPartition(&a, low, high)
      if k == p {
        return a[p]
      } else if k < p {
        return randomizedSelect(&a, low, p - 1, k)
      } else {
        return randomizedSelect(&a, p + 1, high, k)
      }
    } else {
      return a[low]
    }
  }

  precondition(a.count > 0)
  return randomizedSelect(&a, 0, a.count - 1, k)
}

为了保持可读性,功能分为三个内部函数:

  • randomPivot()选择一个随机数并将其放在当前分区的末尾(这是Lomuto分区方案的要求,有关详细信息,请参阅快速排序上的讨论)。

  • randomizedPartition()是Lomuto的快速排序分区方案。 完成后,随机选择的基准位于数组中的最终排序位置。它返回基准值的数组索引。

  • randomizedSelect()做了所有困难的工作。 它首先调用分区函数,然后决定下一步做什么。 如果基准的索引等于我们正在寻找的k元素,我们就完成了。 如果k小于基准索引,它必须回到左分区中,我们将在那里递归再次尝试。 当第k数在右分区中时,同样如此。

很酷,对吧? 通常,快速排序是一种 O(nlogn) 算法,但由于我们只对数组中较小的部分进行分区,因此randomizedSelect()的运行时间为 O(n)

注意: 此函数计算数组中第k最小项,其中k从0开始。如果你想要第k最大项,请用a.count - k

作者:Daniel Speiser,Matthijs Hollemans
翻译:Andy Ron
校对:Andy Ron

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

推荐阅读更多精彩内容