十种排序算法的JS实现

129 阅读5分钟

排序算法作为《数据结构与算法》中最基本的算法,可以分为内部排序和外部排序两大类,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存;本文将要用JS实现十大排序算法,分别是冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、基数排序、桶排序。

1. 冒泡排序

function bubbleSort(nums) {
    // 多次遍历数组,每次遍历都会将最大的数字移至最后(第i项)
    for (let i = nums.length - 1; i > 2; i--) {
	for (let j = 0; j < i; j++) {
	    if (nums[j] > nums[j + 1]) {
            // 如果遇到比后一项数字大的,就交换位置
	        [nums[j + 1], nums[j]] = [nums[j], nums[j + 1]]
	    }
	}
    }
    return nums
}

2. 选择排序

function selectSort(nums) {
    // 从前到后遍历数组,每次从后面数字中选择最小的与当前位置(第i项)数字交换
    let min = null, minIndex = null
    for (let i = 0; i < nums.length - 1; i++) {
	min = nums[i]
	minIndex = i
        // 遍历查找i之后的最小数字
	for (let j = i + 1; j < nums.length; j++) {
	    if (nums[j] < min) {
	        min = nums[j]
		minIndex = j
	    }
	}
        // 如果比第i项数字小,就交换位置
	if (nums[i] > min) {
	    _swap(nums, i, minIndex)
	}
    }
    return nums
}

3. 插入排序

从第二项开始向后遍历,将每一项插入到它前方合适的位置,以此来达到排序的目的

function insertionSort(nums) {
    // 从数组第二项开始遍历数组,向后遍历,把当前项i插入到其前面合适的位置
    for (let i = 1; i < nums.length; i++) {
        // 用cur记录nums[i],因为前一项后移时nums[i]会被覆盖
	let cur = nums[i], j = i - 1
        // while向前循环第i项前面的数字,每一个比cur大的都后移一位,为cur在前方腾出位置
	while (true) {
	    if (nums[j] > cur) {
                // 如果nums[j]比当前项cur大,则后移,腾出前方位置
		nums[j + 1] = nums[j]
                // 边界情况,已经查找到第一项,直接替换
		if (j === 0) {
		    nums[j] = cur
		    break
		}
		j--
	    } else {
                // nums[j]小于等于当前项cur,停止查找,将j后一项替换为cur
		nums[j + 1] = cur
		break
	    }
	}
    }
    return nums
}

4. 希尔排序

希尔排序其实是对插入排序的一种优化,传统插入排序是一项一项向前比较数字的大小,找到合适的位置向前移动插入,但希尔排序通过动态控制遍历间隔,来达到一次性向前多个位置的目的,间隔的设定距离和方式因人而异

function shellSort(nums) {
    const len = nums.length
    let gap = 1
    // while语句保证了初始遍历间隔gap不小于数组长度的1/3,且是3的倍数
    while (gap < len / 3) {
        gap = gap * 3
    }
    // 每一轮遍历,遍历间隔gap都会缩小至原来的1/3,并且最后一轮一定是1 
    for (gap; gap > 0; gap = Math.floor(gap / 3)) {
        // 每一个新的gap,意味着要进行一轮插入排序
        // 每一轮只能移动那些需要向前移动距离大于等于gap的数字
        // 从数组第gap项开始,向后遍历,把当前项i插入到其前面合适的位置
	for (let i = gap; i < len; i++) {
            // 用cur记录nums[i],因为前一项后移时nums[i]会被覆盖
	    let cur = nums[i]
            // 以gap为间隔,for向前遍历第i项前面的数字,每一个比cur大的都后移gap位,
            // 为cur在前方腾出位置
	    for (var j = i; j >= gap && cur < nums[j - gap]; j -= gap) {
	        nums[j] = nums[j - gap]
	    }
            // 找到合适位置后插入cur
	    nums[j] = cur
	}
        // 一轮插入排序结束,此时的数组,每一个数字,需要向前移动的距离都不会超过gap
        // 也就是下一轮不需要向前比较太长的距离,这也是希尔排序的精髓
    }
    return nums
}

5. 归并排序

将数组拆成若干个小数组,小数组先排序,再不断合并成新的有序数组

function mergeSort(nums) {
    // 首先需要一个归并两个有序数组,返回一个有序数组的方法
    function merge(nums1, nums2) {
	nums1 = typeof nums1 === "number" ? [nums1] : nums1
	nums2 = typeof nums2 === "number" ? [nums2] : nums2
        // p1 p2两个指针分别是nums1和nums2的索引,新数组ans
	let p1 = 0, p2 = 0, ans = new Array(nums1.length + nums2.length), count = 0;
        // 当p1小于nums1的长度 且 p2小于nums2的长度,向后遍历nums1和nums2
	while (p1 < nums1.length && p2 < nums2.length) {
            // 因为是从小到大排序,nums1[p1]和nums2[p2]谁小,就先将其录入ans中
	    if (nums1[p1] < nums2[p2]) {
		ans[count++] = nums1[p1++]
	    } else {
		ans[count++] = nums2[p2++]
	    }
        }
        // 此时说明nums1和nums2有一个已经遍历完,
        // 将另外一个剩余的元素直接补至新数组ans之后
	while (p1 < nums1.length) {
	    ans[count++] = nums1[p1++]
	}
	while (p2 < nums2.length) {
	    ans[count++] = nums2[p2++]
        }
        return ans
    }
    // 将一个数组中的元素两两合并,返回一个新的数组
    function sort(list) {
        const ans = new Array(Math.floor(list.length / 2))
        let i = 0, count = 0
	while (i < list.length) {
            // 合并第i项和第i+1项
	    ans[count++] = merge(list[i++], list[i++])
            // 边界情况,list长度为奇数,则将最后一项并入ans最后一个元素当中
	    if (i === list.length - 1) {
                ans[count - 1] = merge(ans[ans.length - 1], list[i++])
	    }
	}
        // 此时ans是list数组中的元素两两合并后的数组
        // ans的每个元素都是一个有序数组
	return ans
    }
    while (nums.length > 1) {
        // 不断对nums做两两合并,直至长度为1
        nums = sort(nums)
    }
    return Array.isArray(nums[0]) ? nums[0] : nums
}

6. 快速排序

从数组中选取一个元素,将小于此元素的数字放在其前方,将大于此元素的数字放在其后方,然后对该元素左右的两个数组分别重复以上操作

function quickSort(nums) {
    if (nums.length < 2) return nums
    // 选取数组最中间的元素mid
    const midIndex = Math.floor(nums.length / 2)
    const mid = nums[midIndex]
    // 创建左右两个数组leftright
    let i = 0, left = [], right = []
    // while循环原数组,将小于mid的元素插入left,大于mid的元素插入right
    while (i < nums.length) {
        if (i !== midIndex) {
	    nums[i] < mid ? left.push(nums[i]) : right.push(nums[i])
        }
	i++
    }
    // 使用递归,对leftright分别重复以上操作,并生成新数组
    return [...quickSort(left), mid, ...quickSort(right)]
}

7. 堆排序

将数组看成一个堆(二叉树),数组的第2i+1项和第2i+2项是第i项的两个子节点,利用大顶堆父节点数字始终大于等于其子节点的特点,依次找出剩余元素中的最大值,进行排序

function heapSort(nums) {
    let len = nums.length
    // 创建大顶堆的方法,大顶堆特征:数组第一位(二叉树顶点)是所有数字里最大的
    function buildMaxHeap() {
	for (let i = Math.floor(len / 2); i >= 0; i--) {
            // 从数组一半开始往前heapify是为了保证将最大值逐步移至第一位
	    heapify(i)
	}
    }
    // heapify方法只做一件事,将第i项与其子节点比较,将较大的数字替换到第i项位置;
    // 使第i项及其所有子孙节点都满足父节点数字大于等于子节点数字(子节点之间不保证大小)
    function heapify(i) {
	const left = 2 * i + 1, right = 2 * i + 2
        // 暂定第i项是最大的数字
	let maximum = i
        // 如果左子节点大于暂定最大值,则暂定左子节点是最大值
	if (left < len && nums[left] > nums[maximum]) {
	    maximum = left
	}
        // 如果右子节点大于暂定最大值,则暂定右子节点是最大值
	if (right < len && nums[right] > nums[maximum]) {
	    maximum = right
	}
        // 判断最大值不是最初的第i项,就将最大值替换到第i项
	if (maximum !== i) {
	    _swap(nums, maximum, i)
            // 此时原第i项被替换到maximum处,为保证父节点最大,
            // 被替换到maximum处的数字继续与其子节点做heapify
	    heapify(maximum)
	}
    }
    // 大顶堆只保证数组第一位(堆顶)是所有数字里最大的,数组还未正确排序
    buildMaxHeap()
    // 利用这一特点逐一将堆顶的元素移至最后,从后到前排序
    for (let i = 0; i < nums.length; i++) {
        // 将最大值(堆顶数字)移至最后
	_swap(nums, 0, --len)
        // 再将新的最大值移至堆顶(此时len已经减1,所以上一个移到最后的值不会被移动)
	heapify(0)
    }
    // 最后会得到一个从小到大排序的数组
    return nums
}

8. 计数排序

记录数组中每一个数字出现的次数,再根据出现次数创建一个新的数组,所以数组中的数字范围不能过大,此方法需要输入数组中最大和最小的数字确定范围

function countingSort(nums, max, min = 0) {
    // 根据最大值和最小值的差距,创建一个记录数组countArr
    const countArr = new Array(max - min + 1).fill(0)
    // 遍历nums数组,在countArr中记录每个数字出现的次数
    for (let i = 0; i < nums.length; i++) {
	countArr[nums[i] - min]++
    }
    let k = 0
    // 根据countArr记录的结果,重新创建一个排序数组nums
    for (let j = 0; j < countArr.length; j++) {
	while (countArr[j]--) {
	    nums[k++] = j + min
	}
    }
    return nums
}

9. 基数排序

遍历数组中的数字,按照每个数字个位数排序一轮,再遍历,按照十位数调整排序,以此类推,直至按照最大的位数调整排序最后一轮,实现排序

function radixSort(nums) {
    // 创建一个长度为10的二维数组,每个元素都为一个空数组
    const counter = new Array(10).fill(0).map(v => [])
    let mod = 10, dev = 1 // 从多位数中每次获取一位数,除数dev同步扩大
    let highBit = true
    let negativeCount = 0 // 记录负数个数
    // highBit记录数组中是否还有更高的位置,如果有,还需遍历一轮
    while (highBit) {
        highBit = false // 假设没有高位
        // 开始一轮遍历
        for (let i = 0; i < nums.length; i++) {
	    let bucket = parseInt(nums[i] / dev) % mod
	    if (bucket < 0) {
                // 只在第一轮记录负数个数即可,并且负数需要取倒数
		dev === 1 && negativeCount++
		bucket = -bucket
	    }
	    // 判断是否有高位,如果有,highBit设为true
	    if (parseInt(nums[i] / (dev * 10)) % mod !== 0) {
	        highBit = true
	    }
            // 以nums[i]此轮指定位(个位、十位...)的数字bucket为索引,
            // 将nums[i]插入到二维数组counter的第bucket个数组中
	    counter[bucket].push(nums[i])
	}
        // 从counter二维数组中,从counter[0][0]开始,依次取出插入的数字
        // 生成新的nums数组,此过程正数要从nums[negativeCount]处往后插入
        // 负数要从nums[negativeCount-1]处往前插入
	let positiveIndex = negativeCount
	let negativeIndex = negativeCount - 1
	for (let i = 0; i < 10; i++) {
	    let index = 0
	    while (typeof counter[i][index] !== 'undefined') {
		if (counter[i][index] >= 0) {
		    nums[positiveIndex++] = counter[i][index++]
		} else {
		    nums[negativeIndex--] = counter[i][index++]
		}
	    }
	    counter[i] = []
	}
	dev *= 10
    }
    return nums
}

10. 桶排序

根据数组中数字的最小值到最大值,创建若干个区间(桶),如【0,5】【5,10】...,遍历数组,将数字按照大小插入到指定的区间(桶)当中,每个桶单独排序,此处因为桶中的元素有明确的大小范围,使用计数排序最合适,最终合并所有的桶

function bucketSort (nums, bucketSize = 10) {
    // 找出数组中元素的最大最小值
    let maximum = nums[0], minimum = nums[0]
    for (let i=0; i<nums.length; i++) {
        if (nums[i] > maximum) {
            maximum = nums[i]
        }
        if (nums[i] < minimum) {
            minimum = nums[i]
        }
    }
    // 从最小值到最大值,创建十个(bucketSize)区间(桶)
    const buckets = new Array(Math.ceil((maximum - minimum) / bucketSize)).fill(null).map(v => [])
    // 遍历nums数组,将元素插入到对应的桶当中
    for (let j=0; j<nums.length; j++) {
        buckets[Math.floor((nums[j] - minimum) / bucketSize)].push(nums[j])
    }
    let m = 0
    for (let k=0; k<buckets.length; k++) {
        let bucket = buckets[k]
        // 每个桶内部进行计数排序,排序后的结果插入到nums数组中
        bucket = countingSort(bucket, (k+1)*bucketSize, k*bucketSize)
        for (let n=0; n<bucket.length; n++) {
            nums[m++] = bucket[n]
        }
    }
    return nums
}