排序算法作为《数据结构与算法》中最基本的算法,可以分为内部排序和外部排序两大类,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存;本文将要用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]
// 创建左右两个数组left和right
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++
}
// 使用递归,对left和right分别重复以上操作,并生成新数组
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
}