JavaScript排序算法
各个算法复杂度
冒泡排序
单向冒泡
function bubbleSort(nums) {
for (let i = 0, len = nums.length; i < len - 1; i++) {
let mark = true
for (let j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
[nums[j], nums[j + 1]] = [nums[j + 1], nums[j]]
mark = false
}
}
if (mark) return
}
}
双向冒泡
function bubbleSort_TwoWays(nums) {
let low = 0
let len = nums.length
while (low < len) {
let mark = true
for (let i = low; i < len; i++) {
if (nums[i] > nums[i + 1]) {
[nums[i], nums[i + 1]] = [nums[i + 1], nums[i]]
mark = false
}
}
len--
for (let i = len; i > low; i--) {
if (nums[i] > nums[i - 1]) {
[nums[i], nums[i - 1]] = [nums[i - 1], nums[i]]
mark = false
}
}
low++
if (mark) return
}
}
选择排序
和冒泡排序相似, 区别在于选择排序是将每一个元素和它后面的元素进行比较和交换。
数据规模越小越好
function selectSort(nums) {
for (let i = 0, len = nums.length;i<len; i++) {
for (let j = i + 1; j < len; j++) {
if (nums[i] > nums[j]) {
[nums[i], nums[j]] = [nums[j], nums[i]]
}
}
}
}
插入排序
以第一个元素作为有序数组,其后的元素通过在这个已有序的数组中找到合适的位置并插入。
function insertSort(nums) {
for(let i=1,len=nums.length;i<len;i++){
let temp = nums[i]
let j=i
while(j>=0&&temp<nums[j-1]){
nums[j]=nums[j-1]
j--
}
nums[j]=temp
}
}
希尔排序
希尔排序是插入排序的一种更高效率的实现。
它与插入排序的不同之处在于, 它会优先比较距离较远的元素。
希尔排序的核心在于间隔序列的设定。 既可以提前设定好间隔序列, 也可以动态的定义间隔序列。
function shellSort(nums) {
let len = nums.length
// 初始步数
let gap = parseInt(len / 2)
// 逐渐缩小步数
while (gap) {
// 从第gap个元素开始遍历
for (let i = gap; i < len; i++) {
// 逐步其和前面其他的组成员进行比较和交换
for (let j = i - gap; j >= 0; j -= gap) {
if (nums[j] > nums[j + gap]) {
[nums[j], nums[j + gap]] = [nums[j + gap], nums[j]]
} else {
break
}
}
}
gap = parseInt(gap / 2)
}
}
快速排序
- 从数组中选择一个元素作为基准点
- 排序数组,所有比基准值小的元素摆放在左边,而大于基准值的摆放在右边。每次分割结束以后基准值会插入到中间去。
- 最后利用递归,将摆放在左边的数组和右边的数组在进行一次上述的1和2操作。
方式一:
- 缺点:
- 首先我们每次执行都会使用到两个数组空间, 产生空间复杂度。
- concat操作会对数组进行一次拷贝, 而它的复杂度也会是O(n)
- 对大量数据的排序来说相对会比较慢
- 优点
- 代码简单明了, 可读性强, 易于理解
- 非常适合用于面试笔试题
function quickSort(arr) {
if (arr.length <= 1) {
return arr
}
let pivotIndex = Math.floor(arr.length / 2);
let pivot = arr.splice(pivotIndex, 1)[0];
let left = []
let right = []
for (var i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat([pivot], quickSort(right))
}
方式二:(优化)
/**
* 分割
* @param {*} A 数组
* @param {*} p 起始下标
* @param {*} r 结束下标 + 1
*
*/
function divide(A, p, r) {
// 基准点
const pivot = A[r - 1]
// i初始化是-1,也就是起始下标的前一个
let i = p - 1
// 循环
for (let j = p; j < r - 1; j++) { // 如果比基准点小就i++,然后交换元素位置
if (A[j] <= pivot) {
i++
[A[i], A[j]] = [A[j], A[i]]
}
}
// 最后将基准点插入到i+1的位置
[A[i + 1], A[r - 1]] = [A[r - 1], A[i + 1]]
// 返回最终指针i的位置
return i + 1
}
//排序
function qsort(A, p = 0, r) {
r = r || A.length
if (p < r - 1) {
const q = divide(A, p, r);
qsort(A, p, q)
qsort(A, q + 1, r)
}
return A
}
归并排序
归并排序是一种非常稳定的排序方法,它的时间复杂度无论是平均,最好,最坏都是NlogN。
- 先拆分,一直拆分到只有一个数
- 拆分完成后,开始递归合并
function mergeSort(nums) {
// 有序合并两个数组
function merge(l1, r1, l2, r2) {
let arr = []
let index = 0
let i = l1
j = l2
while (i <= r1 && j <= r2) {
arr[index++] = nums[i] < nums[j] ? nums[i++] : nums[j++]
}
while (i <= r1) arr[index++] = nums[i++]
while (j <= r2) arr[index++] = nums[j++]
// 将有序合并后的数组修改回原数组
for (let t = 0; t < index; t++) {
nums[l1 + t] = arr[t]
}
}
// 递归将数组分为两个序列
function recursive(left, right) {
if (left >= right) return
// 比起(left+right)/2,更推荐下面这种写法,可以避免数溢出
let mid = parseInt((right - left) / 2) + left
recursive(left, mid)
recursive(mid + 1, right)
merge(left, mid, mid + 1, right)
return nums
}
recursive(0, nums.length - 1)
}
桶排序
取 n 个桶, 根据数组的最大值和最小值确认每个桶存放的数的区间, 将数组元素插入到相应的桶里, 最后再合并各个桶。
图解:
function bucketSort(nums) {
// 桶的个数,只要是正数即可
let num = 5
let max = Math.max(...nums)
let min = Math.min(...nums)
// 计算每个桶存放的数值范围,至少为1,
let range = Math.ceil((max - min) / num) || 1
// 创建二维数组,第一维表示第几个桶,第二维表示该桶里存放的数
let arr = Array.from(Array(num)).map(() => Array().fill(0))
nums.forEach(val => {
// 计算元素应该分布在哪个桶
let index = parseInt((val - min) / range)
// 防止index越界,例如当[5,1,1,2,0,0]时index会出现5
index = index >= num ? num - 1 : index
let temp = arr[index]
// 插入排序,将元素有序插入到桶中
let j = temp.length - 1
while (j >= 0 && val < temp[j]) {
temp[j + 1] = temp[j]
j--
}
temp[j + 1] = val
})
// 修改回原数组
let res = [].concat.apply([], arr)
nums.forEach((val, i) => {
nums[i] = res[i]
})
}
基数排序
使用十个桶 0 - 9, 把每个数从低位到高位根据位数放到相应的桶里, 以此循环最大值的位数次。
但只能排列正整数, 因为遇到负号和小数点无法进行比较。
function radixSort(nums) {
// 计算位数
function getDigits(n) {
let sum = 0;
while (n) {
sum++;
n = parseInt(n / 10);
}
return sum;
}
// 第一维表示位数即0-9,第二维表示里面存放的值
let arr = Array.from(Array(10)).map(() => Array());
let max = Math.max(...nums);
let maxDigits = getDigits(max);
for (let i = 0, len = nums.length; i < len; i++) {
// 用0把每一个数都填充成相同的位数
nums[i] = (nums[i] + '').padStart(maxDigits, 0);
// 先根据个位数把每一个数放到相应的桶里
let temp = nums[i][nums[i].length - 1];
arr[temp].push(nums[i]);
}
// 循环判断每个位数
for (let i = maxDigits - 2; i >= 0; i--) {
// 循环每一个桶
for (let j = 0; j <= 9; j++) {
let temp = arr[j]
let len = temp.length;
// 根据当前的位数i把桶里的数放到相应的桶里
while (len--) {
let str = temp[0];
temp.shift();
arr[str[i]].push(str);
}
}
}
// 修改回原数组
let res = [].concat.apply([], arr);
nums.forEach((val, index) => {
nums[index] = +res[index];
})
}
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序, 计数排序要求输入的数据必须是有确定范围的整数。
function countingSort(nums) {
let arr = []
let max = Math.max(...nums)
let min = Math.min(...nums)
// 加上最小值的相反数来缩小数组范围
let add = -min
for (let i = 0, len = nums.length; i < len; i++) {
let temp = nums[i]
temp += add
arr[temp] = arr[temp] + 1 || 1
}
let index = 0
for (let i = min; i <= max; i++) {
let temp = arr[i + add]
while (temp > 0) {
nums[index++] = i
temp--
}
}
}
堆排序
堆排序可以说是一种利用堆的概念来排序的选择排序。 分为两种方法:
- 大顶堆: 每个节点的值都大于或等于其子节点的值, 在堆排序算法中用于升序排列
- 小顶堆: 每个节点的值都小于或等于其子节点的值, 在堆排序算法中用于降序排列
function heapSort(nums) {
// 调整最大堆,使index的值大于左右节点
function adjustHeap(nums, index, size) {
// 交换后可能会破坏堆结构,需要循环使得每一个父节点都大于左右结点
while (true) {
let max = index
let left = index * 2 + 1 // 左节点
let right = index * 2 + 2 // 右节点
if (left < size && nums[max] < nums[left]) max = left
if (right < size && nums[max] < nums[right]) max = right
// 如果左右结点大于当前的结点则交换,并再循环一遍判断交换后的左右结点位置是否破坏了堆结构(比左右结点小了)
if (index !== max) {
[nums[index], nums[max]] = [nums[max], nums[index]]
index = max
} else {
break
}
}
}
// 建立最大堆
function buildHeap(nums) {
// 注意这里的头节点是从0开始的,所以最后一个非叶子结点是 parseInt(nums.length/2)-1
let start = parseInt(nums.length / 2) - 1
let size = nums.length
// 从最后一个非叶子结点开始调整,直至堆顶。
for (let i = start; i >= 0; i--) {
adjustHeap(nums, i, size)
}
}
buildHeap(nums)
// 循环n-1次,每次循环后交换堆顶元素和堆底元素并重新调整堆结构
for (let i = nums.length - 1; i > 0; i--) {
[nums[i], nums[0]] = [nums[0], nums[i]]
adjustHeap(nums, 0, i)
}
}