基础排序
冒泡排序
最好时间复杂度O(n),最坏时间复杂度O(n^2),平均复杂度O(n^2)。
// 常规解法
function bubbleSort (nums) {
const len = nums.length
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1; j++) {
if (nums[j] > nums[j + 1]) {
[ nums[j], nums[ j + 1 ] ] = [ nums[ j + 1 ], nums[j] ]
}
}
}
return nums
}
// 优化:外层轮次到第 i 次时,代表已经有 i 个元素已经冒泡完成。内层比对可以进行。
function bubbleSort (nums) {
const len = nums.length
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
[ nums[j], nums[ j + 1 ] ] = [ nums[ j + 1 ], nums[j] ]
}
}
}
return nums
}
// 优化:针对本身数组就是有序数组时,一个轮次就知道是否有序。
function betterBubbleSort (nums) {
const len = nums.length
let flag = false
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
// 发生交换则标志非有序
flag = true
[ nums[j], nums[ j + 1 ] ] = [ nums[ j + 1 ], nums[j] ]
}
}
// 一轮后若标志未发生变化则代表数组本身有序
if (!flag) {
return nums
}
}
return nums
}
选择排序
循环遍历数组,每次找出范围内的最小值,把它放在当前范围内的头部,然后缩小范围,继续上述描述,直至数组有序。内外层循环都不可变避免,三个时间复杂度都为O(n^2)。
function selectSort (nums) {
const len = nums.length
// 记录当前区间的最小值索引
let minIndex
for (let i = 0; i < len - 1; i++) {
// 初始化最小值为当前区间第一个元素
minIndex = i
for (let j = i; j < len; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j
}
}
// 如果最小值不是当前区间的第一个元素,则与第一个元素发生交换
if(minIndex !== i) {
[ nums[i], nums[minIndex] ] = [ nums[minIndex], nums[i] ]
}
}
return nums
}
插入排序
基于当前元素前面的序列是有序的前提,从后往前去寻找当前元素在该序列中的正确位置。最好时间复杂度为O(n),最坏时间以及平均时间复杂度为O(n^2)。
function insertSort (nums) {
const len = nums.length
// 缓存当前需要去插入的元素
let temp
// 默认第一个元素有序,从第二个开始插入
for (let i = 1; i < len; i++) {
// 用 j 来标记该元素的正确位置
let j = i
temp = nums[i]
// 判断前一个元素如果比当前插入元素大,则让位,再往前对比
while (j > 0 && nums[j - 1] > temp) {
nums[j] = nums[j - 1]
j--
}
// 对比结束后 j 的位置就是该插入元素的正确位置
nums[j] = temp
}
return nums
}
非基础排序
掌握分治的思想,将大问题分解为若干子问题分别求解,再子问题的解整合为大问题的解。
归并排序
归并排序的时间复杂度 O(nlog(n))。
- 分解子问题:将需要被排序的数组从中间分割为两半,然后再将分割出来的每个子数组各分割为两半,重复以上操作,直到单个子数组只有一个元素为止。
- 求解每个子问题:从粒度最小的子数组开始,两两合并、确保每次合并出来的数组都是有序的。(这里的“子问题”指的就是对每个子数组进行排序)。
- 合并子问题的解,得出大问题的解:当数组被合并至原有的规模时,就得到了一个完全排序的数组。
function mergeSort (nums) {
const len = nums.length
// 处理边界情况
if (len <= 1) {
return nums
}
let mid = Math.floor(len / 2)
// 递归分割左子数组,然后合并为有序数组
const leftNums = mergeSort(nums.slice(0, mid))
// 递归分割右子数组,然后合并为有序数组
const rightNums = mergeSort(nums.slice(mid))
// 返回合并后的有序数组
return mergeNums(leftNums, rightNums)
}
function mergeNums (nums1, nums2) {
let i = 0, j = 0
let res = []
const len1 = nums1.length
const len2 = nums2.length
while (i < len1 && j < len2) {
if ( nums1[i] < nums2[j] ) {
res.push(nums1[i])
i++
} else {
res.push(nums2[j])
j++
}
}
if (i < len1) {
return res.concat( nums1.slice(i) )
} else {
return res.concat( nums2.slice(j) )
}
}
快速排序
快排仍然是分治的思想,区别于归并排序,快排是在原数组内部排序,不会真正的分割数组再合并数组。
function quickSort (nums, left = 0, right = nums.length - 1) {
// 定义递归边界
while (nums.length > 1) {
const lineIndex = partition(nums, left, right)
// 左子数组长度大于 1, 则递归排序
if (left < lineIndex - 1 ) {
quickSort(nums, left, lineIndex - 1)
}
// 右子数组长度大于1, 则递归排序
if (right > lineIndex) {
quickSort(nums, lineIndex, right)
}
}
return nums
}
// 以基准值为轴心 划分左右子数组的过程
function partition (nums, left, right) {
// 基准值默认选取中间元素
const pivotValue = nums[ Math.floor(left + (rigth + left) / 2) ]
// 初始化左右指针
let i = left
let j = right
while (i <= j) {
// 左指针所指元素小于基准值
while (nums[i] < pivotValue) {
i++
}
// 右指针所指元素大于基准值
while (nums[j] > pivotValue) {
j++
}
// 寻找到打破两个while循环的元素,进行交换
if (i <= j) {
swap(nums, i, j)
i++
j--
}
}
return i
}
// 交换数组元素的方法
function swap(arr, i, j) { [arr[i], arr[j]] = [arr[j], arr[i]] }
快速排序的时间复杂度的好坏,是由基准值来决定的。
- 最好时间复杂度:它对应的是这种情况——我们每次选择基准值,都刚好是当前子数组的中间数。这时,可以确保每一次分割都能将数组分为两半,进而只需要递归 log(n) 次。这时,快速排序的时间复杂度分析思路和归并排序相似,最后结果也是 O(nlog(n))。
- 最坏时间复杂度:每次划分取到的都是当前数组中的最大值/最小值。大家可以尝试把这种情况代入快排的思路中,你会发现此时快排已经退化为了冒泡排序,对应的时间复杂度是 O(n^2)。
- 平均时间复杂度: O(nlog(n))