排序算法
排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定排序方式进行排列的一种算法。 ——维基百科
实际应用
具体例子
面试题 16.16. 部分排序
1. 冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢”浮”到数列的顶端。
算法步骤
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
演示
代码实现
// 从头到尾遍历
function bubbleSort(arr) {
const len = arr.length
// 计算遍历总次数
let count = 0
for (let i = 0; i < len; i++) {
// 外层循环每遍历一次,数组尾部排序完成的部分的长度都会+1
for (let j = 0; j < len - 1 - i; j++) {
count++
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
console.log(count)
return arr
}
// 从尾到头遍历
function bubbleSort1(arr) {
const len = arr.length
// 计算遍历总次数
let count = 0
for (let i = len - 1; i >= 0; i--) {
// 外层循环每遍历一次,数组尾部排序完成的部分的长度都会多出一位
for (let j = 0; j <= i - 1; j++) {
count++
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
console.log(count)
return arr
}
如果数组本身就已经是排序好的,或者在排序的过程中就已经排序好了,后面的遍历都是无用的,所以可以加一个排序状态来判断是否排序完成:
function bubbleSort2(arr) {
const len = arr.length
// 计算遍历总次数
let count = 0
// 判断是否排序完成 新增
let isCompelete;
for (let i = len - 1; i >= 0; i--) {
// 外层遍历每次都重置为true 新增
isCompelete = true;
// 外层循环每遍历一次,数组尾部排序完成的部分的长度都会多出一位
for (let j = 0; j < i; j++) {
count++
if (arr[j] > arr[j + 1]) {
// 如果内层遍历有位置交换则表示排序未完成 新增
isCompelete = false;
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
if (isCompelete) break
}
console.log(count)
return arr
}
2. 选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
演示
代码实现
function Selection(arr) {
const len = arr.length;
if (len < 2) return;
let temp = 0;
for (let i = 0; i < len; i++) {
temp = i
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[temp]) {
temp = j
}
}
[arr[i], arr[temp]] = [arr[temp], arr[i]]
}
}
3. 插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
演示
代码实现
function insertionSort(arr) {
const len = arr.length
let preIndex, current
for (let i = 1; i < len; i++) {
preIndex = i - 1
current = arr[i]
// 拿之前的每一项和当前项都进行对比,直到小于等于当前项或者下标为0
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex+1] = arr[preIndex]
preIndex--
}
// 当执行到这里时,preIndex < 0 || arr[preIndex] <= current
// preIndex+1是为了把current插入到preIndex后
arr[preIndex+1] = current
}
return arr
}
4. 归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:递归、迭代
算法步骤
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
重复步骤 3 直到某一指针达到序列尾;
演示
代码实现
function mergeSort(arr) {
if (arr.length < 2) return arr.slice()
var mid = Math.floor(arr.length / 2)
// 递归调用
var left = mergeSort(arr.slice(0, mid))
var right = mergeSort(arr.slice(mid))
var result = []
// 比较left和right的第一项的大小
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
// left和right都为空或者其中一个为空
result.push(...left, ...right)
return result
}
5. 快速排序
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。
算法步骤
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
演示
代码实现
function fastSort(arr) {
// 只剩1个元素,不能再分割了
if (arr.length < 2) return arr
// 取第1个元素为基准值
const base = arr[0]
// 分割为左小右大两个数组,以及包含元素本身的中间数组
let left = [], middle = [base], right = []
for(let i = 1; i < len; i++) {
if (arr[i] === base) {
// 如果有与本身一样大的元素,放入 middle 数组,解决重复元素的问题
middle.push(arr[i])
} else if (arr[i] > base) {
right.push(arr[i])
} else {
left.push(arr[i])
}
}
// 递归并连接
return fastSort(left).concat(middle, fastSort(right))
}
6. 计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法步骤
找出待排序的数组中最大和最小的元素
统计数组中每个值为i的元素出现的次数,存入数组C的第i项
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
演示
代码实现
function countingSort(arr) {
let countArr = [], resultArr = []
// 统计出现次数
for (let i = 0; i < arr.length; i++) {
countArr[arr[i]] = countArr[arr[i]] ? countArr[arr[i]] + 1 : 1
}
// 遍历统计数组,放入结果数组
for (let i = 0; i < countArr.length; i++) {
while (countArr[i] > 0) {
resultArr.push(i)
countArr[i]--
}
}
return resultArr
}
7. 桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
1.在额外空间充足的情况下,尽量增大桶的数量
2.使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
演示
代码实现
function bucketSort(arr, num = 5) {
let buckets = [],
min = Math.min(...arr),
max = Math.max(...arr)
// 初始化 num 个桶
for (let i = 0; i < num; i++) buckets[i] = []
// (最大值-最小值)/桶数,得到每个桶最小最大值的差,即区间
// 比如 range 为10, 0号桶区间为0-10,1号桶10-20,...
let range = (max - min + 1) / num
debugger
for (let i = 0; i < arr.length; i++) {
// (元素-最小值)/区间,取整数部分,就是应该放入的桶的索引
let bucket_index = Math.floor((arr[i] - min) / range),
bucket = buckets[bucket_index]
// 空桶直接放入
if (bucket.length === 0) {
bucket.push(arr[i])
} else {
// 非空,插入排序
let len = bucket.length - 1
while (len >= 0 && bucket[len] > arr[i]) {
bucket[len + 1] = bucket[len]
len--
}
bucket[len + 1] = arr[i]
}
}
// 合并所有桶
let result = []
buckets.forEach((bucket) => {
result = result.concat(bucket)
})
return result
}
8. 基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。