[路飞]取第 k 个最大/最小元素的通用方法-基于快排和堆

记录 1 道算法题

数组中的第 K 个大元素

leetcode-cn.com/problems/kt…


ps:堆在另一篇文章有详细的介绍。不想重复写大家跳过去看吧。这道题用堆的代码会直接写在最后。

这道题最简单的方法就是排序之后返回下标为 k - 1 的元素。

    function findKthLargest(nums, k) {
        nums.sort((a, b) => b - a)
        return nums[k - 1]
    }
复制代码

这样似乎过于简单,我们可以手写一些实现,例如通过快排,或者堆来答题。

1. 快排

快排其实就是找一个基准点,然后把大于它的数放它左边,小于它的数放它右边。左边还是右边取决于你想升序还是降序。

这种把一个大的问题切割成一个个小问题的递归方式,叫做分治。递归的终止条件就是区间内只有一个元素,就直接返回了。

但是我们会发现其实只要排序保证前 k 个是降序的或者升序的,我们就知道第k大元素或第k小元素是什么。所以我们要的第k个数在基准点左边的时候,我们递归处理左边的数组,反之递归右边的数组,这样就减掉了一半的工作量。如果第k个刚好是基准点,就直接返回了不做任何递归操作。

        [3, 2, 5, 6, 7]
        
        假入我们的基准点是  5, 然后要第 2 大元素
        
        我们只需要递归处理 3,2
复制代码

快排比较直观的写法是每次递归生成两个数组,然后返回的时候做拼接。例如:

    // 伪代码
    const left = []
    const right = []
    if (arr[i] > 基准点) {
        right.push(arr[i])
    } else {
        left.push(arr[i])
    }
    
    return [...递归(left), 基准点, ...递归(right)]
复制代码

另一种空间复杂度为1的操作是直接在原数组上进行排序,但是怎么做节点的交换是个关键而且有难度的问题。也是这篇文章的重点之一。

这时候要指针出场了,而且为了方便交换,我们需要把基准点设置到数组的开头 arr[0] 上面。

先假定我们有一个交换的函数 swap,传入数组和下标就会自动进行交换。

总是固定那数组的第一个作为基准点也行,随机取一个数做基准点也行,这里使用随机取一个数做基准点的做法。

首先,因为是在原数组上进行操作,所以我们只有区间的起始下标和终点下标,不一定是 0 和 arr.length - 1。

    // 取这个区间的随机下标
    let pivotIdx = Math.floor(Math.random() * (right - left + 1)) + left
    const pivot = arr[pivotIdx]
    swap(arr, left, pivotIdx)
    pivotIdx = left
    left += 1

    /*
     * 实现的效果是 a 为基准点, left 和 right 为排序的区间。
     * [a, b, c, d, e, f]
     *    left        right
     */
复制代码

然后,使用for循环遍历区间内的节点,比 a 大时进行交换,这时候我们需要有一个新的指针,他指着可以被交换的边界。这里我们可以用 left,因为他已经没有其他作用了。

当我们判断到比基准点大时,进行当前for循环的 i 节点与 left 的交换。交换完之后left++,通过慢慢缩小的方法,确定了 left 之前的值比基准点大。因为一旦发现比基准点大就马上进行交换,所以分区就出现了。

另外,将决定升序或降序的条件抽取出来,规则和 sort 一样。这里用到了降序 compare = (a, b) => b - a

    for(let i = left; i <= right; i++) {
        if (compare(arr[i], pivot) < 0) {
            swap(arr, i, left)
            left++
        }
    }
    
    // 当分割完之后,我们已知上一次的 left 是最后一次交换后自增1。
    // 那 left - 1 就是基准点左边的第一个元素。假如 [4, 5, 6, 3, 2], 6是最后一次交换。
    // 他和基准点进行交换,就刚好把基准点放入到正确的位置。 [6, 5, 4, 3, 2]
    swap(arr, pivotIdx, left - 1)
    // 然后返回基准点的下标
    return left - 1
    
    // 下面这些是升序排列的例子, 当比基准点小时进行交换
    // [3, 4, 6, 2, 1]
        /* 
            3  i 1  index 1 -- 4 < 3 
            4  i 2  index 1 -- 6 < 3
            6  i 3  index 1 -- 2 < 3 --> index 2 [3, 2 , 6, 4, 1]
            2  i 4  index 2 -- 1 < 3 --> index 3 [3, 2, 1, 4, 6]
            1  i 5 break

            基准点交换 [1, 2, 3, 4, 6]
            2
          */

        // [3,2,1,5,6,4]  2
        /* 
            3  i  index  1

            2 i 1 index 1  2 < 3 --> index--2  [3, 2, 1, 5, 6, 4]
            1 i 2 index 2  1 < 3 --> index--3  [3, 2, 1, 5, 6, 4]
            5 i 3 index 3  5 < 3
            6 i 4 index 3  6 < 3
            4 i 5 index 3  4 < 3

            基准点交换 0 2   [1, 2, 3, 5, 6, 4]  return 2
          */
复制代码

分割完就要进行递归操作了。递归要做的事情很简单,重复对数组进行分割。这一步要实现减治,首先我们需要知道第 k 个元素的下标,所以作为函数的参数传进来。然后根据第k个元素的下标在基准点的左边还是右边,进行二选一的递归操作。

    function quickSort(arr, k, left = 0, right = arr.length) {
        const 基准点下标 = 分割(arr, left, right)
        // 这里是判断到底取基准点左边的区间还是基准点右边的区间,但是都不包括基准点本身
        if (基准点下标 < k) {
            quickSort(arr, k, left, 基准点下标 - 1)
        } else if (基准点下标 > k) {
            quickSort(arr, k, 基准点下标 + 1, right)
        }
    }
复制代码

同时为了处理递归终止的条件,我们需要 left < right,他们不能相等,至少区间内有两个元素。

以上就是快排的解析,完整代码如下。

    function findKthLargest(nums, k) {
        quickSort(nums, k - 1)

        return nums[k - 1]
    }

    function quickSort(arr, k, left = 0, right = arr.length - 1) {
        if (left < right) {
          const pivotIdx = partition(arr, left, right)
          if (pivotIdx > k) {
            quickSort(arr, k, left, pivotIdx - 1)
          } else if (pivotIdx < k) {
            quickSort(arr, k, pivotIdx + 1, right)
          }
        }

        return arr
    }
    
    // 升序降序的判断
    const compare = (a, b) => b - a
    function partition(arr, left, right) {
        let pivotIdx = Math.floor(Math.random() * (right - left + 1)) + left
        const pivot = arr[pivotIdx]
        swap(arr, pivotIdx, left)
        pivotIdx = left
        left = left + 1

        for (let i = left; i <= right; i++) {
          if (compare(arr[i], pivot) < 0) {
            swap(arr, i, left)
            left++
          }
        }

        swap(arr, pivotIdx, left - 1)
        return left - 1
    }

    function swap(arr, n1, n2) {
        ;[arr[n1], arr[n2]] = [arr[n2], arr[n1]]
    }
复制代码

再次ps:堆在另一篇文章有详细的介绍。不想重复写大家跳过去看吧。

这道题完整代码如下:

    function findKthLargest(nums, k) {
        const heap = new Heap()
        for(let i = 0; i < nums.length; i++) {
            // 堆顶是最大堆里面最小的元素,比他还小就直接跳过
            // 最小堆则相反
            if(heap.size() === k && heap.data[0] > val ) {
                continue
            }
            heap.push(nums[i])
            if (heap.size() > k) {
                heap.pop()
            }
        }
        
        return heap.data[0]
    }
    
    class Heap {
        constructor() {
          this.data = []
          this.compare = (a, b) => a - b
        }

        size() {
          return this.data.length
        }

        swap(n1, n2) {
          const { data } = this
          const temp = data[n1]
          data[n1] = data[n2]
          data[n2] = temp
        }

        push(val) {
          this.data.push(val)
          this.bubblingUp(this.size() - 1)
        }

        pop() {
          if (this.size() === 0) return null
          const { data } = this
          const discard = data[0]
          const newMember = data.pop()
          if (this.size() > 0) {
            data[0] = newMember
            this.bubblingDown(0)
          }

          return discard
        }

        bubblingUp(index) {
          while (index > 0) {
            const parent = (index - 1) >> 1
            const { data } = this
            if (this.compare(data[index], data[parent]) < 0) {
              this.swap(parent, index)
              index = parent
            } else {
              break
            }
          }
        }

        bubblingDown(index) {
          const { data } = this
          const last = this.size() - 1
          while (true) {
            const left = index * 2 + 1
            const right = index * 2 + 2
            let parent = index
            if (left <= last && this.compare(data[left], data[parent]) < 0) {
              parent = left
            }
            if (right <= last && this.compare(data[right], data[parent]) < 0) {
              parent = right
            }
            if (index !== parent) {
              this.swap(index, parent)
              index = parent
            } else {
              break
            }
          }
        }
      }
复制代码

Supongo que te gusta

Origin juejin.im/post/7048561599724355592
Recomendado
Clasificación