无处不在的排序算法

208 阅读5分钟

简单的排序算法

1 排序是最常见的算法,基本上每本书开头都是先介绍常用的各种排序算法。

排序算法总体来说,只是种类繁多,但是基本上都不是很难,都是些基础,这些多写,打好基础就可以了。

215 快速选择

题目描述

在一个未排序的数组中,找到第 k 大的数字。

注意,这里你可以默认肯定有解

例子1

输入: [3,2,1,5,6,4] and k = 2
输出: 5

例子2

输入: [3,2,3,1,2,4,5,5,6] and k = 4
输出: 4

思考 1

基本上先排序,然后直接输出就可以了

这里也可以自己实现,比如使用快速排序,一般快排是查找第k大数的常用方法

实现1

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */

export default (nums, k) => {
  nums.sort((a, b) => b - a);
  return nums[k - 1];
};

实现2

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */

const getPosition = (nums, p, r) => {
  const mid = Math.floor(p + (r - p) / 2);
  let temp;
  let pos;
  let tempArr = [nums[p], nums[r], nums[mid]].sort((a, b) => a - b);
  temp = tempArr[1];
  pos = temp === nums[p] ? p : temp === nums[r] ? r : mid;
  for (let i = p; i <= r; i++) {
    if (nums[i] < temp && i > pos) {
      const _temp = nums[i];
      if (i > pos) {
        for (let j = i; j > pos; j--) {
          nums[j] = nums[j - 1];
        }
      }
      nums[pos] = _temp;
      pos++;
    } else if (nums[i] > temp && i < pos) {
      nums[pos] = nums[i];
      nums[i] = temp;
      pos = i;
    }
  }
  nums[pos] = temp;

  return pos;
};
// p 起始位置, r 结束位置
// 3, 2, 3, 1, 2, 4, 5, 5, 6
const quickSort = (nums, p, r) => {
  if (p < r) {
    const q = getPosition(nums, p, r);
    quickSort(nums, p, q - 1);
    quickSort(nums, q + 1, r);
  }
};
export default (nums, k) => {
  // nums.sort((a, b) => a - b);
  quickSort(nums, 0, nums.length - 1);
  return nums[nums.length - k];
};

实现1的算法时间复杂度 O(nlgn), 空间复杂度 O(1)

451. 根据字符出现频率排序

题目描述

给定一个字符串,请将字符串里的字符按照出现的频率降序排列。

例子1

输入: "tree"
输出: "eert"
解释: 'e'出现两次,'r'和't'都只出现一次。 因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案

例子2

输入: "cccaaa"
输出: "cccaaa"
解释: 'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。注意"cacaca"是不正确的,因为相同的字母必须放在一起。

例子3

输入: "Aabb"
输出: "bbAa"
解释: 此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。 注意'A'和'a'被认为是两种不同的字符。

思考 1

题目也比较简单,只要思想类似于桶排序,我们先用一个hashMap存储每个字符出现的次数,然后对hashMap进行排序,最后再取出来就可以了。

实现1

/**
 * @param {string} s
 * @return {string}
 */

export default (s) => {
  const map = new Map();
  for (let i = 0; i < s.length; i++) {
    if (!map.has(s[i])) {
      map.set(s[i], 1);
    } else {
      const count = map.get(s[i]) + 1;
      map.set(s[i], count);
    }
  }
  let res = [];
  for (let [key, value] of map) {
    res.push({
      key,
      value,
    });
  }
  res.sort((a, b) => b.value - a.value);
  let res1 = "";
  for (let i = 0; i < res.length; i++) {
    res1 += res[i].key.repeat(res[i].value);
  }
  return res1;
};

实现1的算法时间复杂度 O(nlgn), 空间复杂度 O(1) 这里空间复杂度是O(1)的原因是最多只有26个字母,所以不会一直扩张

347. 前 K 个高频元素

题目描述

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

例子1

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

例子2

输入: nums = [1], k = 1
输出: [1]

提示:
1 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
2 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
3 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
4 你可以按任意顺序返回答案。

思考 1

桶排序顾名思义就是我们事先放几个桶,这几个桶是排好序的,然后我们可以把符合每个桶的元素放到每个桶里边,这样就天然变成有序的了

这里我们可以根据数组中每个不同的数字分别设置一个桶,每个桶里放入数字出现的次序,最后返回钱k个高频元素就可以了

实现1

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */

export default (nums, k) => {
  const map = new Map();
  for (let i = 0; i < nums.length; i++) {
    if (!map.has(nums[i])) {
      map.set(nums[i], 1);
    } else {
      const count = +map.get(nums[i]) + 1;
      map.set(nums[i], count);
    }
  }
  let res = [];
  for (let [key, value] of map) {
    res.push({
      key,
      value,
    });
  }
  res.sort((a, b) => b.value - a.value);
  res = res.slice(0, k);
  return res.map((item) => +item.key);
};

实现1的算法时间复杂度 O(nlgn), 空间复杂度 O(1)

75. 颜色分类

题目描述

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

进阶:

1 你可以不使用代码库中的排序函数来解决这道题吗?
2 你能想出一个仅使用常数空间的一趟扫描算法吗?

例子1

输入: nums = [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]

例子2

输入: nums = [2,0,1]
输出: [0,1,2]

例子3

输入: nums = [0]
输出: [0]

例子4

输入: nums = [1]
输出: [1]

提示:
1 n == nums.length
2 1 <= n <= 300
3 nums[i] 为 0、1 或 2

思考 1

这里使用桶排序,直接统计0,1,2出现的次数,直接修改原数组就可以了

当然这里还有其它方法,比如使用双指针,因为前面使用过双指针,我们可以实现下

双指针也很简单,一个low指向数组的开始,一个high指向数组的末尾,使用i进行遍历数组,如果发现nums[i]等于0,则和low交换,并且low++,当发现等于2,则和high交换并且high--

实现1

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
// [2, 0, 2, 1, 1, 0][(2, 0, 2, 1, 1, 0)];
export default (nums) => {
  const res = new Array(3).fill(0);
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] === 0) {
      res[0]++;
    } else if (nums[i] === 1) {
      res[1]++;
    } else {
      res[2]++;
    }
  }
  // console.log(res);
  for (let i = 0; i < res[0]; i++) {
    nums[i] = 0;
  }
  for (let i = 0; i < res[1]; i++) {
    nums[res[0] + i] = 1;
  }
  for (let i = 0; i < res[2]; i++) {
    nums[res[0] + i + res[1]] = 2;
  }
};


实现2


/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
// [2, 0, 2, 1, 1, 0][(2, 0, 2, 1, 1, 0)];
const swap = (nums, i, j) => {
  const temp = nums[i];
  nums[i] = nums[j];
  nums[j] = temp;
};

export default (nums) => {
  let low = 0;
  while (nums[low] === 0) {
    low++;
  }
  let high = nums.length - 1;
  while (nums[high] === 2) {
    high--;
  }
  for (let i = low; i <= high; ) {
    if (low > high) {
      break;
    }
    if (nums[i] === 0 || nums[i] === 2) {
      if (nums[i] === 0) {
        if (low !== i) {
          swap(nums, low, i);
        }
        low++;
        i = low;
      } else if (nums[i] === 2) {
        if (i !== high) {
          swap(nums, i, high);
        }
        high--;
      }
      continue;
    } else {
      i++;
    }
  }
  return nums;
};

实现1的算法时间复杂度 O(n), 空间复杂度 O(1)

实现2的算法时间复杂度 O(n), 空间复杂度 O(1)

排序算法总结

现实中使用的排序算法其实不多,大多数是作为基础,考验个人的细心和基础,这些一般都是作为基础算法,来磨练基本功的。

基本上了解其思想就可以。