排序

144 阅读4分钟

快速选择

利用了快速排序的思想,求解第 K 个元素的问题。利用的是快速排序的划分思想 partition() 进行实现的。需要先打乱数组,不然最坏的情况复杂度为 O(N^2)。

快速排序的每次 partition() 过程中都排好一个位置。如果排好的是正数第K个位置,那么它就是第K小的元素。

预备知识:

  • 堆是一个完全二叉树
  • 完全二叉树:除开最后一层,其他层的节点数都达到最大,最后一层的所有节点都集中在左边。
  • 大根堆:根节点为最大值,每个结点的值大于等于孩子结点的值。
  • 小根堆:根节点的值为最小值,每个根节点的值都小于等于孩子结点的值。
  • 堆的存储:用数组来存储。
  • 对于节点i,子节点为2i+1 与 2i+2
  • 堆排序算法(以大根堆(heapify)为例):
    1. 对每一个非叶子节点,从下至上,从右至左,做 shiftDown 下沉操作。操作后根节点为最大值,将其与最后一个结点交换。
    2. 除开最后一个节点,其余节点继续转化成大根堆,此时根节点为次最大,将其与最后一个结点交换。
    3. 重复2,直到堆中元素个数为1,排序完成。
const swap = (arr, i, j) => {
  [arr[i], arr[j]] = [arr[j], arr[i]]
}

const shiftDowm = (arr, i, length) => {
  for (let j = 2 * i + 1; j < length; j = j * 2 + 1) {
    let temp = arr[i]
    if (j + 1 < length && arr[j + 1] > arr[j]) {
      j++
    }
    if (arr[j] > temp) {
      swap(arr, i, j)
      i = j
    } else {
      break;
    }
  }
}

const heapSort = (arr) => {
  for (let i = Math.floor(arr.length / 2 - 1); i >= 0; i--) {
    shiftDowm(arr, i, arr.length)
  }
  for (let i = arr.length - 1; i >= 0; i--) {
    swap(arr, 0, i)
    shiftDowm(arr, 0, i)
  }
}

用于求解第 K 个最小元素问题。可以维护一个大小为K的大根堆。大根堆的堆顶元素即为该堆的最大值。每次遍历新元素时,如果比堆顶元素小,则把堆顶元素去除,将新元素加到堆中,调整堆。遍历完之后,堆里面的K个元素是从小到大排列的,堆顶即为第K个最小元素。

也可以用于求解第K个最大元素的问题。维护一个K大小的小根堆。

JS 实现堆排序

Kth Element

LeetCode 215

Input: [3,2,1,5,6,4] and k = 2 Output: 5

题目描述:找出倒数第K大的元素

排序

时间复杂度 O(NlogN),空间复杂度 O(1)

var findKthLargest = function(nums, k) {
    nums.sort();
    return nums[nums.length-k]
};

先用自带的排序方法进行排序,然后再取倒数第k个元素

时间复杂度 O(NlogK),空间复杂度 O(K)。

快速排序

时间复杂度 O(N),空间复杂度 O(1)

var findKthLargest = function (nums, k) {
  k = nums.length - k
  let l = 0, r = nums.length - 1
  while (l < r) {
    let res = partition(nums, l, r)
    if (res === k) break
    else if (res > k) r = res - 1
    else l = res + 1
  }
  return nums[k]
};


const partition = (nums, i, j) => {
  let l = i, r = j + 1
  while (true) {
    while (l < j && nums[++l] < nums[i]);
    while (r > i && nums[--r] > nums[i]);
    if (l >= r) break;
    [nums[l], nums[r]] = [nums[r], nums[l]]
  }
  [nums[i], nums[r]] = [nums[r], nums[i]]
  return r
}

通过每次划分,找出倒数第k个元素就行。

出现频率最多的 k 个元素

LeetCode 347

Given [1,1,1,2,2,3] and k = 2, return [1,2].

设置若干个桶,桶的下标表示出现的频率,即第 i 个桶里存储的数出现的频率为 i。都存到桶里之后,根据桶的频率进行排序,从后往前遍历,最先得到的k个元素即为出现频率最多的k个元素。

var topKFrequent = function (nums, k) {
  let map = new Map()
  for (i of nums) {
    map.set(i, map.get(i) + 1 || 1)
  }
  let sortedArray = [...map.entries()].sort((a, b) => b[1] - a[1])

  let res = []
  for (let i = 0; i < k; i++) {
    res.push(sortedArray[i][0])
  }
  return res
};

按照字符串出现次数对字符串进行排序

LeetCode 451

Input: "tree" Output: "eert"

同上题,先用桶存所有字符,按照字符的频率进行排序,从最多频率往最少频率进行打印即可。

var frequencySort = function (s) {
  let map = new Map()
  for (let i of s) {
    map.set(i, map.get(i) + 1 || 1)
  }
  let res = [];
  [...map.entries()].sort((a, b) => b[1] - a[1]).forEach(cur => {
    for (let i = 0; i < cur[1]; i++) {
      res.push(cur[0])
    }
  })
  return res.join('')
};

荷兰国旗问题

LeetCode 75

Input: [2,0,2,1,1,0] Output: [0,0,1,1,2,2]

题目描述:按照 0 1 2 顺序进行排序,不能使用原始的排序

运用划分的思想,等于零的放左边,等于二的划分到右边,最后中间就是等于1的了。

var sortColors = function (nums) {
  let zero = -1, one = 0, two = nums.length
  while (one < two) {
    if (nums[one] === 0) {
      zero++
      [nums[zero], nums[one]] = [nums[one], nums[zero]]
      one++
    } else if (nums[one] === 2) {
      two--
      [nums[two], nums[one]] = [nums[one], nums[two]]
    } else {
      one++
    }
  }
  return nums
};