马上就要过年了,趁着这段长假好好地研究一下基础的排序算法,先来看一张图
- 稳定性:若是相邻的两个数字相等,排序后不会影响(交换)他们的位置
- 不稳定性:相邻的两个数字相等,排序后有可能交换他们的位置
- 内排序:所有排序都在内存中操作
- 外排序:因为数据太大 ,需要把数据放在磁盘中,排序操作通过磁盘和内存的数据传输来运行
- 时间复杂度:一个算法所耗费的时间
- 空间复杂度:运行完一个程序所占内存的大小
注意:下面的排序实例,都是将排序方法定义在数组的prototype
上,这样使用的时候即可直接使用.(点)
运算符调用,如下
Array.prototype.methods = function(){
//......
}
arr.methods()//使用点运算符调用
1.插入排序
实现原理(这里指升序排列):
- 选取数组的第二项与之前的数作比较,若之前的项 > 第二项,则交换位置并插入当前空位
- 选取第三项,与第三项之前的项两两比较,若是比较的项 > 第三项,则交换位置,当比较的项 < 第三项时,则插入当前空位
let arr = [1, -1, 2, 4, 8, 0, 3]
Array.prototype.insert = function(){
for(let i = 1; i < this.length; i++){
let putItem = this[i] //被提出的数组项
for(let j = i - 1; j >= 0 && putItem < this[j]; j--){
//将提出的项与之前的项两两比较,若满足条件,则交换项目
this[j+1] = this[j]
this[j] = putItem
}
}
}
arr.insert()
console.log(arr) //[-1, 0, 1, 2, 3, 4, 8]
2.冒泡排序
实现原理(这里指升序排列):
- 将数组的左右项目两两比较,将较大数字放在靠数组的右边。
- 上一步完成后,这时数组的最后一项就是数组的最大项目。
- 以此类推,排序操作完成后,数组升序排列
//冒泡排序
let arr = [0, -1, 1, 10, 4]
Array.prototype.sorty = function(){
for (var i = 0; i < this.length-1; i++) {
for(var j = 0; j < this.length-1; j++){
if (this[j] > this[j+1]){
console.log(arr)
var temp = this[j]
this[j] = this[j+1]
this[j+1] = temp
};
}
};
}
arr.sorty()
console.log(arr)//[-1, 0, 1, 4, 10]
3.归并排序
归并排序与之前的排序方式相比会难以理解一些,先来看一张图
实现原理(这里指升序排列):分为两部分("分"
与 "治"
)
- 第一部分:分
像上面图片所述的,将一个多数项的数组分为两个数组,直到单项数字
- 第二部分:治
将单项数字依次组合成多个只有两项的小数组,并排序,这时数组的左边就是最小的数字,之后再进行合并数组,并排序。直到排序完毕。
//这里定义的函数,可以将拆分开的数组进行排序
function merge(left, right){
var newArr = []
var i = 0;
var j = 0;
while(i < left.length && j < right.length){
//比较数组的头部,并将符合条件的数字推入新数组
if(left[i] < right[j]){
newArr.push(left[i++])
}else{
newArr.push(right[j++])
}
}
//上面循环完毕后,会出现其中一个数组还有残留的数项(剩余的数项可以直接推入新数组)
//下面将剩下的数都推入新数组
while(i < left.length) {
newArr.push(left[i++])};
while(j < right.length) {
newArr.push(right[j++])}
return newArr
}
写完后可以自己代入两个数组试一试
"治"
实现了,下面来实现"分"
function Merge_sort(arr){
if(arr.length == 1) return arr
//当数组长度为一时,代表 "分" 成功,返回当前数项
let point = Math.floor(arr.length/2)
let leftArr = arr.slice(0, point)
let rightArr = arr.slice(point)
return Merge(Merge_sort(leftArr),Merge_sort(rightArr)) //递归,直到数组长度唯一
}
console.log(Merge_sort([0, -1, 1, 10, 4, -4, 800]))
//left: [-1] right: [1]
//left: [0] right: [-1, 1]
//left: [10] right: [4]
//left: [-4] right: [800]
//lrft: [4, 10] right: [-4, 800]
//left: [-1, 0, 1] right: [-4, 4, 10, 800]
// [-4, -1, 0, 1, 4, 10, 800]
从打印的数据也可以看出,将拆分的单项数字,合并成两项的数组比较大小并排序,再合并成多项,以此类推。最后两项数组合并为一项并排序完毕。
4.快速排序
快速排序由于排序效率在同为
O(N*logN)
的几种排序方法中效率较高,因此经常被采用,也经常会到各大公司的面试题见到。同时它也是具有一定难度的排序算法。
实现原理(这里指升序排列):
- 先从数列中取出一个数作为基准数(这里取第一位)。
- 将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
- 再对左右区间重复第二步(递归调用),直到各区间只有一个数。
详细的步骤点击
建议看比较官方的网站讲解,博主看了几个视频,感觉讲得全都不一样。
function quick_sort(arr, start, end){
let i = start
let j = end
let temp = arr[i] //取出基准数
while(i<j){
while(i < j && arr[j] >= temp) j-- // 从右向左找第一个小于temp的数
if(i < j){
arr[i] = arr[j]
}
while(i < j && arr[i] < temp) i++ //从左向右找第一个大于temp的数
if(i < j){
arr[j] = arr[i]
}
console.log(arr)
}
arr[i] = temp // 将基准数放入数组中
//上面的代码执行完后,基数右边都是比基数小的数,基数左边都是比基数大的数字
//下面的判断语句将基数左右两边的数字继续相同的操作(递归调用)
if (i > start) quick_sort(arr, start, i - 1);
if (j < end) quick_sort(arr, j + 1, end);
return arr
}
let a = quick_sort([2, 3, 1, -1, -1, 56, 3, 5], 0, 7)
console.log(a) //[-1, -1, 1, 2, 3, 3, 5, 56]
5.选择排序
经过几天的基础算法学习,简单的算法也已经可以看完原理,自己实现了。
实现原理(这里指升序排列):
- 选择数列中最小的数字,放到数列的第一个位置,
- 再排除第一个数字(最小的数字),选择这个数列中的最小的数字,放到数列第二位中(既第二位为最小的数字)
- 重复完毕后,排序完毕
简单来说,就是找到最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
Array.prototype.choose_sort = function(){
for(let i = 0; i < this.length; i++){
//规定整体循环的次数,并规定比较参照数(i)
for (let j = i + 1; j < this.length; j++) {
//将 i 与数组后的每一项比较,满足条件则交换
if (this[i] > this[j]) {
let temp = this[i];
this[i] = this[j];
this[j] = temp;
};
};
}
return this
}
console.log([1, 3, 4, 4, -1, 3].choose_sort()) //[-1, 1, 3, 3, 4, 4]
6.希尔排序
希尔排序又叫做
缩小增量排序
,是上面讲到的直接插入排序的改进版。
希尔排序的平均排序性能也要优于插入排序
实现原理(这里指升序排列):
- 选取一个增量(gep),这里选取数组长度的一半。增量会逐次缩小
- 根据增量,将数列分为不同的子序列,分别对子序列插入排序
- 最后当增量为 1 时,进行最后一次插入排序。完毕
希尔排序与很多排序不同,它使用增量来分割子序列,理解需要花费一些时间。
在实现这个排序时,因为开始没有理解透彻,编写代码时,也踩了许多坑。
下面的代码是我所理解还没透彻时,编写的问题代码
function shell(arr){
let gep = Math.floor(arr.length/2)
for (; gep > 0; gep = Math.floor(gep/2)) {
//规定增量,并分割子序列
for (let j = gep; j < arr.length; j++) {
// 将子序列进行插入排序
let i = j - gep
while(i >= 0 && arr[i] > arr[j]){
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
};
};
console.log(arr)
}
shell([2,5,1,5,6,23,33,2,1,10]) //[1, 1, 2, 5, 2, 5, 6, 10, 23, 33]
上述代码的输出结果并没有将数组排序成功。
找了许久资料和问题后发现,问题出在子序列的插入排序上。
上面的代码只是将子序列数字两两比较(类似于冒泡排序),没有使用插入排序,也并不满足冒泡排序的条件。当然就无法排序成功
下面是改良之后的代码
function shell2(arr){
let gep = Math.floor(arr.length/2) // 声明增量
for (; gep > 0; gep = Math.floor(gep/2)) {
//规定增量,并分割子序列
for (let j = gep; j < arr.length; j++) {
let i = j
let temp = arr[j] //选取子序列的第二位进行插入排序
while(i - gep >= 0 && arr[i - gep] > temp){
arr[i] = arr[i - gep]
i = i - gep
arr[i] = temp
}
};
};
console.log(arr)
}
shell2([2,5,1,5,6,23,33,2,1,10]) //[1, 1, 2, 2, 5, 5, 6, 10, 23, 33]
上面第七行的temp = arr[i]
,是在while
循环体外进行缓存的,之前踩了大坑,错误的将它缓存至while
内部,每一次缓存arr[i]
准备交换数字时,因为之前的循环可能将arr[i]
替换了位置,导致排序错误
7.计数排序
对于已经确定范围的数列,使用计数排序是明智且高效的选择。(一般用作范围小于100的排序,最高效)
计数排序也是用于 确定范围的整数的线性时间排序算法
实现原理(这里指升序排列):
- 规定索引数组的长度(为数列中最大值的数字 + 1)
- 计算数列中每个数字出现的次数,并计入索引数组中
- 根据索引数组,将排序好的数列推入一个空数组
计数排序的概念有一些抽象,刚刚开始我也是一头雾水。
但是多看了几遍视频将原理弄明白之后,还是很容易实现的
function count_sort(arr, maxValue){
let len = arr.length
let bucketArr = new Array(maxValue + 1) //以数组的最大值设置bucketArr的长度
let resultArr = []
for (let i = 0; i < len; i++) {
//判断原数组中每个元素出现的个数,并累加
if (!bucketArr[arr[i]]) bucketArr[arr[i]] = 0 ;
bucketArr[arr[i]]++
};
for (var j = 0; j < len; j++) {
// 将bucketArr中的排序,填充至resultArr
while(bucketArr[j] > 0){
resultArr.push(j)
bucketArr[j]--
}
};
console.log(resultArr)
}
count_sort([2,2,2,5,5,2,6,8,9,9,1,3], 9) //[1, 2, 2, 2, 3, 5, 5, 6, 8, 9, 9]