写在前面
- 本文主要归纳js常见的几种排序方法,包括冒泡排序,插入排序,快速排序等等。
1.冒泡排序
- 这里实现的方式是使用双层for循环,分为内层循环和外层循环;
1.1 内层循环
- 核心是每次循环时遍历数组中的所有元素,两两比较相邻的元素,如果后面的数小于前面的数,那么交换两数的位置;
- 例如【5,3, 2】,3个数字会比较两次,首先是5比3大,那么相互调换位置得到【3,5,2】;接着5比2大,再次换位最终得到【3,2,5】;
- 因此每一次循环都会将数组中未排序的最大的数字排在数组的最后一个;
function bubbleSort(arr) {
// 获取数组长度
let len = arr.length
// 此循环执行一次,就会找出最大的数字排在数组最后
for (let i = 0; i < len - 1; i++) {
if (arr[i] > arr[i + 1]) {
// 交换位置, 传统写法
const temp = arr[i]
arr[i] = arr[i + 1]
arr[i + 1] = temp
// 交换位置可以使用es6数组结构方式
// ;[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]]
}
}
return arr
}
let list = [9, 7, 6, 5, 3]
console.log(bubbleSort(list))
1.2 外层循环
- 在上面循环中,实现了将数组中最大的数排在数组末尾位置;但是要实现将所有数字排序,我们需要将排序的循环执行4次(以上面list数组中存在5个元素的情况下);
- 因此我们会需要使用for循环将此排序逻辑多次执行,最终得到下面代码:
function bubbleSort(arr) {
// 获取数组长度
let len = arr.length
// 外层循环
for (let j = 0; j < len - 1; j++) {
// 此循环执行一次,就会找出最大的数字排在数组最后
for (let i = 0; i < len - j - 1; i++) {
if (arr[i] > arr[i + 1]) {
// 交换位置, 传统写法
;[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]]
}
}
}
return arr
}
//测试例子
let list = [9, 7, 6, 5, 3]
console.log(bubbleSort(list))
let list2 = [2, 45, 55, 66, 7, 32, 3, 44, 55,23,674, 78, 99]
console.log(bubbleSort(list2))
以上逻辑结合list 例子解释一下:
- list数组有5个元素,内层循环每次执行完会排好一个数字,5个数字全部排序需要内层循环需要执行4次,因为排好4个数字后最后一个自然也就排好了;所以外层循环初始值 j=0, 终止条件为 j < 4 ,也就是内层循环执行4次;
- 内层循环的结束条件为什么是
i < len - j -1
,为什么需要减去j,
以 list = [9, 7, 6, 5, 3] 为例子:
当 j = 0 , i < 5 - 0 - 1 = 4
此时list中5个数都还未排序,数组中元素两两比较需要比较4次,最终得到 [ 7, 6, 5, 3, 9 ]当j = 1 ,i < 5 - 1 - 1 = 3
此时list中4个数是未排序的,那么数组中元素两两比较只需要比较3次就够,最终得到 [ 6, 5, 3, 7, 9 ]- 接下来以此类推…
- 最终结论就是变量 j 控制内层循环的次数,而内层循环每次遍历只需要比较数组中未排序的元素即可。
2 选择排序
- 算是冒泡排序的改进版,在排序之前选择一个数作为基准,将这个数与其后面的数进行两两比较,找到最小值与它进行位置互换
2.1 内层循环
- 下面例子取数组中第一个元素作为基准,循环找到数组中最小的数与其进行位置交换
// 选择排序
function selectionSort(arr) {
let len = arr.length
// 定义一个变量,保存数组中最小值的下标,这里假设数组第一个元素是最小值
let min = 0
// 循环比较时就从第二元素开始比较
for (let i = min + 1; i < len; i++) {
if (arr[min] > arr[i]) {
// 最小数的下标赋值给min
min = i
}
// 交换位置
;[arr[0], arr[min]] = [arr[min], arr[0]]
}
return arr
}
let list = [9, 7, 6, 5, 3]
console.log(selectionSort(list))
2.2 加上外层循环
- 这里通过外层循环变量依次取数组中的元素作为基准,每次循环执行完将未排序的元素中的最小元素排到数组首位(这里的数组首位不包括已排好序的元素)
- 例如
list = [9, 7, 6, 5, 3]
,第一次循环已经把3排到了数组首位[3,9,7,6,5]
,第二次循环则是余下的数进行排序,即5会被排到3后面
function selectionSort(arr) {
let len = arr.length
for (let j = 0; j < len - 1; j++) {
// 定义一个变量,保存数组中最小值的下标,这里假设数组第一个元素是最小值
let min = j
// 循环比较时就从第二元素开始比较
for (let i = min + 1; i < len; i++) {
if (arr[min] > arr[i]) {
// 最小数的下标赋值给min
min = i
}
//交换位置
;[arr[min], arr[j]] = [arr[j], arr[min]]
}
}
return arr
}
let list = [9, 7, 6, 5, 3]
console.log(selectionSort(list))
3 插入排序
- 每次循环结束,数组前面的有序数就会多一个
- 从数组第二个元素开始,每次取出一个元素与前面元素进行比较,数字大的放后面
function insertionSort(arr) {
let len = arr.length
// 从第二个元素开始,每次循环都会将一个元素排序
for (let i = 1; i < len; i++) {
// 暂存当前遍历的元素
let temp = arr[i]
// 定义变量 j 并将 i 赋值给 j;本质就是将数组中下标位置为 j 之前的元素一一和 temp元素进行比较
let j = i
// 从数组中下标为 j-1 位置的元素开始与temp元素进行比较,如果大于temp元素且j>0
// 那么把较大值赋值给下标为 j 的元素,本质就是把较大元素向后排列
while (arr[j - 1] > temp && j > 0) {
arr[j] = arr[j - 1]
j--
}
// 将temp元素插入到正确位置
arr[j] = temp
}
return arr
}
let list = [9, 7, 6, 5, 3]
console.log(insertionSort(list))
- 结合例子分析步骤:
- 第一次遍历
i = 1 temp=7 j=1
9>7 把9赋值给7得到9 9 6 5 3
终止while循环后将temp=7
赋值给第一个元素;得到结果7 9 6 5 3
- 此时
arr=[7,9,6,5,3]
,在此基础上进行;第二次遍历i = 2 temp=6 j=2
9>6 把9赋值给6 ; 得到结果7 9 9 5 3
;
j=1 7>6
把7赋值给arr[1];j=0 ; 结果为7 7 9 5 3
终止while循环; 把temp值6赋值给arr[0],得到 最终6 7 9 5 3
- 第一次遍历
- 接下来的以此类推。。。
4 希尔排序
- 此排序是插入排序的改进版,引入了一个增量概念
- 在每一次循环,间隔指定距离(增量)的元素进行插入排序,当增量为1时就是算法就是插入排序了
- 本质上就是将插入排序外层for循环的的初始值变为了动态变量gap
4.1 代码实现
let list = [9, 7, 6, 5, 3]
function shellSort(arr) {
let len = arr.length
if (!len) return []
// 定义增量,向下取整 gap=2
let gap = Math.floor(len / 2)
// 增量小于1结束
while (gap > 0) {
// 循环排序
for (let i = gap; i < len; i++) {
const temp = arr[i]
let j = i
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap]
j -= gap
}
arr[j] = temp
}
// 改变增量,再次for循环,直到gap<1结束
gap = Math.floor(gap / 2)
}
return arr
}
console.log(shellSort(list))
5 快速排序
- 核心思路就是在数组中选定一个基准元素,首次循环将比基准元素小的和大的分别放置在基准元素的左右两侧
- 将基准元素两侧的元素作为递归调用自身函数时的入参,实现所有数据的排序
- 可以理解为,挑选一个基准元素,将剩下元素分为两块,一块是都比它大的元素,另一块是都比它小的元素;
5.1 代码实现
- 将
list
数组元素从小到大排序,定义right, left
两个数组用于存储比基准元素(piovt)大的和小的元素
let list = [23, 4, 13, 10, 100, 7, 12, 72, 76]
function quickSort(arr) {
if (arr.length <= 1) return arr
// 计算基准元素在数组中的下标
const pivotIndex = Math.floor(arr.length / 2)
// 得到基准元素
const pivot = arr[pivotIndex]
// 定义缓存数组
const left = []
const right = []
for (let i = 0; i < arr.length; i++) {
// 遍历到基准元素时跳过缓存逻辑,避免基准元素被添加至缓存数组中
if (i === pivotIndex) continue
if (arr[i] < pivot) {
// 比基准元素小的放left数组
left.push(arr[i])
} else {
// 比基准元素大的放right数组
right.push(arr[i])
}
}
console.log(left)
console.log(pivot)
console.log(right)
console.log('**********************')
// 将缓存数组作为函数入参递归调用自身,最终返回组合后的数组
return [...quickSort(left), pivot, ...quickSort(right)]
}
console.log('jieguo: ', quickSort(list))
5.2 获取基准元素的优化
- 上面的例子直接是取的数组中间的元素,但是这种方式容易造成比较极端的情况,例如中间那个元素就是数组中的最大值或者最小值
- 这里为了避免这种情况,我们在选取基准元素时,取数组中的头尾以及中间元素进行比较,取它们的中位数作为基准元素
//修改之后,quickSort方法获取piovt元素使用getPiovt方法
function quickSortll(arr) {
//...
const pivot = getPiovt(arr, pivotIndex)
//...
}
// 接收两个参数,arr为数组,center为数组中间元素的下标值
function getPiovt(arr, center) {
let left = 0
let right = arr.length - 1
// 判断这三个数字,并且从小到大排,两两交换位置
if (arr[left] > arr[center]) {
;[arr[left], arr[center]] = [arr[center], arr[left]]
}
if (arr[center] > arr[right]) {
;[arr[center], arr[right]] = [arr[right], arr[center]]
}
if (arr[left] > arr[right]) {
;[arr[left], arr[right]] = [arr[right], arr[left]]
}
// 返回基准元素
return arr[center]
}