算法尽显编程之美
ps:以下排序都默认进行升序排列
一,冒泡排序
算法描述:每次对两个元素进行比较,每遍历一次都能找到一个最小值,直到没有需要交换的,也就是说排序完成。
描述:
- 比较相邻的两个元素,如果第一个元素大于第二个元素,则交换他们的位置。
- 对每个相邻的元素进行相同的工作,直到结尾的最后一对元素。这样最后的元素是最大的数。
- 针对所有元素重复上述操作,除了最后一个(不成对了)。
- 持续每次对越来越少的元素重复上述操作,直到没有任何一对数字需要比较。
实现代码:
console.time('冒泡执行时间')
function bubbleSort(arr){
const len = arr.length
for(let i=0; i < len - 1; i++){
for(let j=0; j < len - i - 1; j++){
// len - i 每次遍历一次后都会排出一个最大值,因此不需要对后面的有序数据在进行比较
if(arr[j] > arr[j+1])
// 解构赋值, 进行快速切换值,不需要额外的创建变量
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]
}
}
return arr
}
let data = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15, 36, 26, 27]
console.log(bubbleSort(data))
console.timeEnd('冒泡执行时间')
// [2, 3, 3, 4, 5, 5, 15, 15, 19, 26, 26, 27, 27, 36, 36, 38, 38, 44, 44, 46, 47, 47, 48, 50]
改进冒泡排序:
- 每次循环,分别从两端同时进行排序(正反冒泡),以减少排序次数。
function bubbleSort(arr){
// 初始化两端
let start = 0, end = arr.length
while(start < end){ // 如果头 大于 尾,则排序完成
// 正向冒泡
for(let i=start; i < end; i++){
if(arr[i] > arr[i+1]){
[arr[i], arr[i+1]] = [arr[i+1], arr[i]]
}
}
// 反向冒泡
for(let i=end; i > start; i--){
if(arr[i] < arr[i-1]){
[arr[i], arr[i-1]] = [arr[i-1], arr[i]]
}
}
start++
end--
}
return arr
}
let data = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15, 36, 26, 27]
console.log(bubbleSort(data))
// [2, 3, 3, 4, 5, 5, 15, 15, 19, 26, 26, 27, 27, 36, 36, 38, 38, 44, 44, 46, 47, 47, 48, 50]
图示
二,选择排序
算法描述:在未排序序列中找最小元素,放在排序序列的起始位置,然后,再从剩余未排序序列中继续寻找最小元素,放在已排序序列的末尾,直到未排序序列没有元素,也就是说排序完成。
描述:
- 首先在未排序序列找到最小元素,放在排序序列的起始位置。
- 再从剩余未排序序列中继续找到最小元素,然后放在已排序序列的末尾。
- 重复以上操作,直到未排序序列没有元素。
实现代码:
console.time('选择排序')
function selectSrot(arr){
// 初始化最小元素的索引
let minIndex, len = arr.length
for(let i=0; i < len - 1 ; i++) {
// 默认最小元素索引
minIndex = i
for(let j=i+1; j < len; j++){// i+1 每次遍历,已排序序列都会加一个元素
if(arr[j] < arr [minIndex]) // 对默认最小元素 与 当前元素进行比较
// 如果当前元素 小于 默认最小元素 那么把当前元素的索引 设置成最小元素索引
minIndex = j
}
// 遍历完成后,找出最小元素的索引,把最小元素放到已排序序列中
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(selectSrot(arr))
console.timeEnd('选择排序')
图示
三, 归并排序
算法描述:采用分治法,对传入待排序序列进行递归拆分,当拆分到每个子序列有序(每个子序列只剩一个元素)时,进行回归,使子序列段间有序,在使两个有序表合成一个有序表。
描述:
- 把长度为n的待排序序列,拆分成两个长度为n/2的子序列。
- 对两个子序列分别采用归并排序。
- 将两个排好的子序列合并成一个最终的有序序列。
实现代码:
console.time('归并排序')
function mergeSort(arr){
const len = arr.length
if(len < 2) // 递归终止条件
return arr
let [left, right] = [arr.splice(0, Math.floor(len / 2)), arr]
return merge(mergeSort(left), mergeSort(right)) // 对待排序序列进行拆分
}
function merge(left, right){
let result = []
while(right.length && left.length){
let item = left[0] >= right[0] ? right.shift() : left.shift()
result.push(item)
}
while(right.length)
result.push(right.shift())
while(left.length)
result.push(left.shift())
return result
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(mergeSort(arr))
console.timeEnd('归并排序')
图示
四, 快速排序
算法描述:本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法,在待排序序列,选一个元素作为基准,所有小于这个基准的都移到基准左边,反之亦然,对基准左右两边的子集,不断重复相同的动作,直到序列有序。
描述:
- 在待排序序列,选一个元素作为基准。
- 所有小于这个基准的都移到基准左边,反之亦然。
- 对基准左右两边的子集,不断重复1,2步,直到序列有序。
console.time('快速排序')
function fastSort(arr, left = 0, right = arr.length - 1) {
if(left < right){ // 结束递归条件
console.log('fastSort',left, right, arr)
let partitionPos = partition(arr, left, right) // 对序列进行拆分
console.log(partitionPos)
// 对拆分后的序列,继续进行相同操作
fastSort(arr, left, partitionPos-1)
fastSort(arr, partitionPos+1, right)
}
return arr
}
function partition(arr, left, right){
const pivot = left
let index = pivot + 1
for(let i=index; i <= right; i++){
if(arr[i] < arr[pivot]){ // 与基准进行比较
[arr[i], arr[index]] = [arr[index], arr[i]]
index++ // 基准偏移量(有多少小于基准的元素移动到基准左侧)
}
}
[arr[pivot], arr[index - 1]] = [arr[index - 1], arr[pivot]]
return index - 1
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(fastSort(arr))
console.timeEnd('快速排序')
改进快速排序:
- 对传入待排序序列进行递归拆分,并对其进行排序,当拆分到每个子序列有序(每个子序列只剩一个元素)时,进行回归。
console.time('快速排序')
function fastSort(arr) {
const len = arr.length
if(len < 2) return arr // 递归结束条件(当待排序序列只剩一个元素)
const index = Math.floor(len / 2) // 获取中心点(基准)
const pivot = arr.splice(index, 1)[0] // 提取出基准
let left = [], right = []
console.log(len)
for(let i=0; i < len - 1; i++){
console.log(len)
// 当前元素与基准进行比较
arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i])
}
return fastSort(left).concat([pivot], fastSort(right))
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(fastSort(arr))
console.timeEnd('快速排序')
图示
五, 插入排序
算法描述:通过构建有序序列,对未排序序列,在已排序序列中从后向前扫描找到位置并插入。从后向前扫描过程中,需要反复把已排序元素逐步向后挪移,为最新元素提供插入空间。
描述:
- 从第一个元素开始(该元素可认为已被排序)。
- 取出下一个元素,在已排序序列中从后向前扫描。
- 如果已排序元素大于新元素,将新元素移到一下位置。
- 重复步骤3,直到找到已排序元素小于或等于新元素的位置。
- 将新元素插入到该位置。
- 重复2~5,直到序列有序。
实现代码:
console.time('插入排序')
function insertSort(arr){
const len = arr.length
for(let i=1; i < len; i++){ // 第一个元素,默认已被排序
let tmp = arr[i] // 获取新元素
for(let j=i; j >= 0; j--){ // 从后向前扫描
if(tmp < arr[j-1]){ // 当前元素 与 新元素进行比较
// 新元素小于当前元素,把当前元素向后挪移,为新元素提供插入空间
arr[j] = arr[j-1]
}else{
arr[j] = tmp // 新元素大于当前元素,将新元素移到一下位置,然后插入
break
}
}
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(insertSort(arr))
console.timeEnd('插入排序')
改进插入排序:
- 查找插入位置时使用二分查找的方式。
console.time('插入排序')
function insertSort(arr){
const len = arr.length
for(let i=1; i < len; i++){
let tmp = arr[i], left = 0, right = i-1 // 初始化左右两端位置及新元素
while(left <= right){ // 结束条件,不能再拆分时
const middle = Math.floor((left+right) / 2)
tmp < arr[middle] ? right = middle - 1 : left = middle + 1 // 新元素 与 当前元素比较;(小于:0~middle-1,大于:middle+1~right)
}
for(let j=i; j >= left; j--){
arr[j] = arr[j-1] // 新元素小于当前元素,把当前元素向后挪移,为新元素提供插入空间
}
arr[left] = tmp
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(insertSort(arr))
console.timeEnd('插入排序')
图示
六,希尔排序
算法描述:将待排序序列,分成若干子序列并进行直接插入排序,待整个序列基本有序,在对序列进行直接插入排序。
描述:
- 选择一个增量,按增量序列个数k,进行k躺排序。
- 每趟排序对应一个增量,分别对按增量分成的若干子序列进行直接插入排序。
- 当增量为1时,对序列进行插入排序,使序列有序。
实现代码:
console.time('希尔排序')
function shellSort(arr, spacing = 3){ // spacing 序列间隔 spacing >= 2
const len = arr.length
let gap = 1 // 增量因子
while(gap < len / spacing){ // 动态生成增量
gap *= spacing + 1
}
for(; gap > 0; gap = Math.floor(gap / spacing)){ // gap 增量因子,由大到小
console.log(gap)
for(let i=0; i < len; i++){
let tmp = arr[i], j = i - gap // j:下一个元素的位置
for(; j >= 0 && tmp < arr[j]; j -= gap){ // j:为正数,tmp:新元素,arr[j]:当前元素
arr[j + gap] = arr[j]
}
arr[j + gap] = tmp
}
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(shellSort(arr))
console.timeEnd('希尔排序')
图示
七,堆排序
算法描述: 堆排序是指利用堆这种数据结构所设计的一种算法,堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。
描述:
- 创建一个堆。
- 把堆首与堆尾互换(arr[0] 与 arr[arr.length - 1]的值互换)。
- 堆尺寸缩小1。
- 重复2步,直到堆尺寸为1。
代码实现:
console.time('堆排序')
function heapSort(arr){
let heapLen = arr.length // 获取堆的大小
// 初始化堆 堆->顶:i,左:2i+1,右:2i+2
for(let i=Math.floor(heapLen / 2) - 1; i>=0; i--){
heapify(arr, i, heapLen)
}
console.log('初始化堆',JSON.parse(JSON.stringify(arr)))
// 现在要把最大的放到最后一个,最后一个放到第一个。
//把堆的大小减少一个,在慢慢的把最大的循环上去。
for(let j=heapLen - 1; j>=1; j--){
[arr[0], arr[j]] = [arr[j], arr[0]]
heapify(arr, 0, --heapLen)
console.log('每次迭代堆',JSON.parse(JSON.stringify(arr)))
}
return arr
}
function heapify(arr, x, len){
// 堆->顶:x,左:2x+1,右:2x+2
let left = 2*x + 1, right = 2*x + 2, largest = x
// 比较 左 与 父节点(顶)
if(left < len && arr[left] > arr[largest]){
// 把左替换成父节点(顶)
largest = left
}
// 比较 右 与 父节点(顶)
if(right < len && arr[right] > arr[largest]){
// 把右替换成父节点(顶)
largest = right
}
if(largest !== x){ // 如果父节点(顶)改变,那么堆性质可以发生改变,需要维护堆性质
// 如果最大者不是父节点 则交换位置
[arr[x], arr[largest]] = [arr[largest], arr[x]]
heapify(arr, largest, len)
}
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(heapSort(arr))
console.timeEnd('堆排序')
图示
堆排序示意图:
八,计数排序
算法描述:待排序序列,有确定的范围,计数排序使用一个额外的数组C,其中第i个元素是待排序序列中值等于i的元素的个数。然后根据数组C来将待排序序列中的元素排到正确的位置。它只能对整数进行排序。
描述:
- 在待排序序列中找出,最小和最大元素。
- 统计待排序序列每个值为i元素出现的次数,存入数组C的第i项。
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
实现代码:
console.time('计数排序')
function countSort(arr, maxValue){
// 根据待排序序列的最值,初始化计数数组,并都用0填充
let newArr = new Array(maxValue + 1).fill(0)
const len = arr.length
let sortIndex = 0 // 排序位置
// 根据待排序序列的元素,向计数数组进行累计
for(let i=0; i < len; i++){
newArr[arr[i]]++
}
const newLen = newArr.length
for(let j=0; j < newLen; j++){
// 进行方向填充
while(newArr[j] > 0){
arr[sortIndex++] = j
newArr[j]--
}
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(countSort(arr, 50))
console.timeEnd('计数排序')
改进计数排序:
- 找到待排序序列的最大,最小值,根据最大 - 最小的范围创建计数数组。
console.time('计数排序')
function countSort(arr){
const len = arr.length
let sortIndex = 0 // 排序位置
let min = max = arr[0] // 初始化最大,最小值
// 获取待排序序列的最值
for(let k=0; k < len; k++){
min = min < arr[k] ? min : arr[k]
max = max > arr[k] ? max : arr[k]
}
// 根据最值创建计数数组,并用0填充
let newArr = new Array((max - min) + 1).fill(0)
// 根据待排序序列的元素,向计数数组进行累计
for(let i=0; i < len; i++){
newArr[arr[i] - min]++
}
const newLen = newArr.length
for(let j=0; j < newLen; j++){
// 进行方向填充
while(newArr[j] > 0){
arr[sortIndex++] = j + min
newArr[j]--
}
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(countSort(arr))
console.timeEnd('计数排序')
图示
计数排序示意图:
九,桶排序
算法描述:将数据分布到有限数量的桶里,然后对每个桶在分别进行排序,最后把桶的数据拼接起来
描述:
- 根据数据长度,设置一个桶大小,并计算出需要的桶数量。
- 遍历数据,把数据一个一个放进对应的桶里。
- 对每个桶大小 > 1 的进行排序。
- 最后把桶的数据拼接起来。
实现代码:
console.time('桶排序')
function bucketSort(arr, bucketSize){
const len = arr.length
if(len <= 1)
return arr
// 验证输入是否符合要求
let regex = /^[1-9]+[1-9]*$/
let min = max = arr[0] // 初始化最大,最小值
let sortIndex = 0 // 排序位置
// // 获取待排序序列的最值
for(let k=0; k < len; k++){
min = min < arr[k] ? min : arr[k]
max = max > arr[k] ? max : arr[k]
}
// 设置默认桶大小
bucketSize = bucketSize > 1 && regex.test(bucketSize) ? bucketSize : 5
// 动态设置桶数量
let bucketCount = Math.floor((max - min) / bucketSize) + 1
// 初始化桶
let bucketS = new Array(bucketCount).fill(1).map(() => [])
// 向桶中插入数据
for(let i=0; i < len; i++){
bucketS[Math.floor((arr[i] - min) / bucketSize)].push(arr[i])
}
// 重置arr
arr.length = 0
for (i = 0; i < bucketS.length; i++) {
let bucketSItem = bucketS[i], bucketSLen = bucketSItem.length
if(bucketSLen > 0){
arr = arr.concat(shellSort(bucketSItem)) // 对每个桶进行排序,并进行拼接
}
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(bucketSort(arr))
console.timeEnd('桶排序')
图示
十,基数排序
算法描述:其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较,基数排序时按照从低位到高位依次排序,直到最高位。
描述:
- 确定最大位数。
- 基于桶思想,根据基数0-9,创建十个桶,然后从低位到高位依次循环遍历数据,把数据一个一个放进对应的桶里。
- 最后把桶的数据拼接起来。
- 重复2,3步,直到最高位
实现代码:
console.time('基数排序')
function radixSort(arr, maxDigit){ // maxDigit:最大位数
let mod = 10, dev = 1, len = arr.length // mod:取模 dev:除数
for(let i=0; i < maxDigit; i++, mod *= 10, dev *= 10){
// 基于桶思想,根据基数0-9,创建十个桶
let bucketS = new Array(10).fill(1).map(()=>[])
for(let j=0; j < len; j++){
// 向桶中插入数据
// (arr[j] % mod) / dev
// 当maxDigit位:1
// (26 % 10) / 1 => 6 / 1 => 6
// 当maxDigit位:2
// (26 % 100) / 10 => 26 / 10 => 2.6
bucketS[Math.floor((arr[j] % mod) / dev)].push(arr[j])
}
console.log(JSON.parse(JSON.stringify(bucketS)))
let bucketSLen = bucketS.length
arr.length = 0
for(let j=0; j < bucketSLen; j++){
arr = arr.concat(bucketS[j])
}
}
return arr
}
let arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48, 3, 44, 38, 5, 47, 15]
console.log(radixSort(arr, 2))
console.timeEnd('基数排序')
图示
基数排序示意图: