「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
前言
前端算法系列是我对算法学习的一个记录, 主要从常见算法、数据结构、算法思维、常用技巧几个方面剖析学习算法知识, 通过LeetCode平台实现刻意练习, 通过掘金和B站的输出来实践费曼学习法, 我会在后续不断更新优质内容并同步更新到掘金、B站和Github, 以记录学习算法的完整过程, 欢迎大家多多交流、点赞、收藏, 让我们共同进步, daydayup👊
目录地址:目录篇
相关代码地址: Github
相关视频地址: 哔哩哔哩-百日算法系列
一、算法分类
- 比较类: 通过元素之间的比较来实现, 时间复杂度不能突破O(nlogn)
- 非比较类: 不需要比较元素的大小
二、算法复杂度
相关知识补充
- 时间复杂度: 执行代码所需时间与数据量之间的函数关系
- 空间复杂度: 执行代码所需空间与数据量之间的函数关系
- 稳定性: 相等的两个数据排序之后是否顺序不变
三、十大排序算法详解
前言: 代码的实现方式可能略有不同、但主体的思想不变、为了使代码更加简洁抽离出部分功能代码
3.0 冒泡排序(Bubble Sort)
描述
依次比较前后两个元素、将较小的值移动到前面、同时较大的元素会冒泡到末尾
图解
代码
function bubbleSort(nums) {
let len = nums.length
for(let i=len; i>0; i--) {
for(let j=0; j<i-1; j++) {
if (nums[j] > nums[j+1]) temp(nums, j, j + 1)
}
}
}
// 数据解构版位置互换
function temp(nums, i, j) {
[nums[i], nums[j]] = [nums[j], nums[i]]
}
3.1 快速排序(Quick Sort)
描述
选定一个基数(pivot)、将大于等于基数的放右边、小于基数的放左边、然后递归处理
图解
代码
// 本函数以数组的首个元素为基准
function quickSort(nums) {
if (nums.length < 2) return nums
let left = [], right = []
while(nums.length > 1) {
let num = nums.pop()
if (num >= nums[0]) {
right.push(num)
} else {
left.push(num)
}
}
return [...quickSort(left), nums[0], ...quickSort(right)]
}
3.2 选择排序(Selection Sort)
描述
每次从数组中选出最小的元素
图解
代码
function selectionSort(nums) {
let i = 0,
len = nums.length
while(i < len) {
let min = getMinIdx(nums, i)
temp(nums, i, min)
i++
}
return nums
}
// 获取数组最小(注意范围)
function getMinIdx(nums, i) {
let min = 0
for(let j=i; j < nums.length; j++) {
if (nums[j] < nums[min]) min = j
}
return min
}
3.3 堆排序(Heap Sort)
描述
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
图解
代码
// TODO
var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
function buildMaxHeap(arr) { // 建立大顶堆
len = arr.length;
for (var i = Math.floor(len/2); i >= 0; i--) {
heapify(arr, i);
}
}
function heapify(arr, i) { // 堆调整
var left = 2 * i + 1,
right = 2 * i + 2,
largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function heapSort(arr) {
buildMaxHeap(arr);
for (var i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0);
}
return arr;
}
3.4 插入排序(Insertion Sort)
描述
从未排序数组中拿出来一个插入到已排序数组中、类比打扑克的起牌过程
图解
代码
/*
function insertionSort(nums) {
for(let i=1; i<nums.length; i++) {
let j = i - 1
let num = nums[i]
while(j > 0 && num < nums[j]) j--
nums.splice(i, 1)
nums.splice(j + 1, 0, num)
}
return nums
}
*/
for(let i=1; i<nums.length; i++) {
let j = i - 1
let num = nums[i]
while(j >= 0 && num < nums[j]) j--
nums.splice(i, 1)
nums.splice(j + 1, 0, num)
}
return nums
3.5 希尔排序(Shell Sort)
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
描述
插入排序的升级版、先对间隔为 n/2 的数据集比较、再对间隔为 n/2/2 的比较、直到最后比较整个数组
图解
代码
function shellSort(nums) {
let len = nums.length, gap = len
while(gap = getGap(gap)) {
for(let i=0; i<nums.length; i++) {
let k = i, num = nums[i]
while(k - gap >= 0 && num < nums[k - gap]) {
nums[k] = nums[k - gap] // * 复习点
k = k - gap
}
nums[k] = num
}
}
}
// 获取增量
function getGap(len) {
return Math.floor(len/2)
}
3.6 归并排序(Merge Sort)
描述
经典分治算法、先分成两堆分别排序、然后再合并
图解
代码
function mergeSort(nums) {
if (nums.length < 2) return nums
let i = Math.floor(nums.length / 2)
return merge(
mergeSort(nums.slice(0, i)),
mergeSort(nums.slice(i))
)
}
// 合并两个数组
function merge(l, r) {
let res = []
while(l.length || r.length) {
if (!l.length || l[0] > r[0]) {
res.push(r.shift())
} else if (!r.length || l[0] <= r[0]) {
res.push(l.shift())
} else {
//
}
}
return res
}
3.7 计数排序(Counting Sort)
描述
将数据的值作为下标存储在新数组中并计数、然后再取出、计数排序要求输入的数据必须是有确定范围的整数
图解
代码
function countingSort(nums, max) {
let ans = []
let res = new Array(max + 1)
for(let i=0; i<nums.length; i++) {
let k = nums[i]
res[k] = res[k] ? (res[k] + 1) : 1
}
for(let i=0; i<res.length; i++) {
while (res[i]) {
ans.push(i)
res[i] -= 1
}
}
return ans
}
3.8 桶排序(Bucket Sort)
描述
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
图解
代码
function bucketSort(nums) {
const size = 10
const [buckets, getHashMap] = HashMap(nums, size)
for(let i=0; i<nums.length; i++) {
let idx = getHashMap(nums[i])
buckets[idx].push(nums[i])
}
nums.length = 0
for(let i=0; i<buckets.length; i++) {
if (buckets[i].length === 0) continue
// 对桶内的数据排序 这里采用插入排序
insertionSort(buckets[i])
for(let j=0; j<buckets[i].length; j++) {
nums.push(buckets[i][j])
}
}
return nums
}
// 创建桶以及映射函数
function HashMap(nums, size) {
let max = nums[0]
let min = nums[0]
let buckets = new Array(size)
for(let i=1; i<nums.length; i++) {
max = max > nums[i] ? max : nums[i]
min = min < nums[i] ? min : nums[i]
}
for(let i=0; i<buckets.length; i++) {
buckets[i] = []
}
let pivot = Math.floor((max - min) / size) + 1
return [buckets, function(num) {
return Math.floor(num / pivot)
}]
}
// 插入排序
function insertionSort(nums) {
for(let i=1; i<nums.length; i++) {
let j = i - 1
let num = nums[i]
while(j > 0 && num < nums[j]) j--
nums.splice(i, 1)
nums.splice(j + 1, 0, num)
}
return nums
}
3.9 基数排序(Radix Sort)
描述
先比较个位、从0到9划分重组、再比较十位、依次比较
引用: 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
图解
代码
function radixSort(nums) {
let digit = getMax(nums)
for(let i=0; i<digit; i++) {
let newNums = []
let res = new Array(10).fill(1).map(() => [])
for(let j=0; j<nums.length; j++) {
let level = getNum(nums[j], i)
res[level].push(nums[j])
}
for(let k=0; k<10; k++) {
while(res[k].length) {
newNums.push(res[k].shift())
}
}
nums = newNums
}
return nums
}
// 获取当前位的值
function getNum(val, i) {
return (val % Math.pow(10, i + 1)) / Math.pow(10, i)
}
// 获取最大数与其位数
function getMax(nums) {
let max = 0, digit = 0
for(let i=1; i<nums.length; i++) {
if (nums[i] > nums[max]) max = i
}
max = nums[max]
digit = (max + '').length
return digit
}
四、前端TOY版排序方法扩展
function setTimeOutSort(nums) {
let res = []
for(num of nums) {
setTimeOut(() => res.push(num), num)
}
return res
}
五、个人笔记
1.选择排序和冒泡排序的最好时间复杂度为什么不一样
2.堆排序(TODO)、希尔排序、快速排序不熟练
3.每个算法的分析未写