剑指Offer-JavaScript版-part1.md

指offer实战-JavaScript版

前言:以前刚学JS的时候,一直觉得前端不怎么需要算法,就是单纯的实现业务功能,现在想想那时候的想法多少天真,可以说用幼稚来形容了。作为一个合格的程序猿,怎么可能不搞算法,前端的算法不会太复杂,一般都是一些比较简单的算法,对照剑指offer,将里面的算法重新实战一遍,代码位置在另一个仓库Offer

1、二维数组中的查找

题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路:二维数组是有规律的,按照横纵展开,向右向下递增。选取数组右上角的数字,如果该数字等于要查找的数组,则结束。如果大于要查找的数字,则该列都大于要查找的数字,剔除该列,向左移动一列继续查找;如果小于要查找的数字,则该行都小于要查找的数字,剔除该行,向下移动一行继续查找;直到查找到该数字或者查找到左下角还没有匹配,则返回没有匹配。

代码github

1
2
3
4
5
6
7
8
9
10
11
12
13
function find(array, target) {
let row = array.length - 1;
for (let i = row, j = 0; i >= 0 && j < array[i].length;) {
if (target === array[i][j]) {
return true
} else if (target > array[i][j]) { //目标大于,则下移一行
j++; //向下移动一行
} else {
i--; //向左一动一行
}
}
return false
}

2、替换空格

题目:请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

思路:当然是直接用正则匹配然后使用replace替换就好了。

代码

1
2
3
4
function replaceSpace(str) {
let reg = new RegExp(/\s/g);
return str.replace(reg, '--')
}

3、从尾到头打印链表

题目:输入一个链表,从尾到头打印链表每个节点的值。

不懂链表的可以参考《学习JavaScript的数据结构与算法》

思路:先将链表每个结点的值存入数组中,然后通过数组的reverse方法,即可从尾到头打印

代码:Github

1
2
3
4
5
6
7
8
function printLinkedList(head) {
let arr = [];
while (head !== null) {
arr.push(head);
head = head.next();
}
return arr.reverse();
}

4、重建二叉树

题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路

代码:Github


5、用两个栈来实现一个队列

题目:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型

思路:栈的操作是后入先出(LIFO),队列的操作是先入先出(FIFO)

代码:Github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let arr1 = [], arr2 = [], result = [];

function push(head) {
arr2.push(head);
return arr
}

function shift() {
if (arr1.length) {
arr1.shift()
} else if (arr2.length) {
arr2.shift()
} else {
return null
}
return arr
}

6、旋转数组的最小数字

题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

思路:其实剑指Offer中想要表达的是一种寻找数组最小值的二分查找方法,旋转之后原数组分为两个部分,[3,4,5,1,2],前面的部分最小的数肯定大于或等于后面部分最大的数,通过这一点可以节省遍历,典型的二分法。

代码:Github

1
2
3
4
5
6
7
8
function rotate(arr) {
return arr.sort(function (a, b) {
if (a < b)
return -1;
else return 1
})[0];
// return Math.min.apply(arr, arr);
}

7、斐波那契数列

题目:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。n<=39

思路:传统思路是使用递归,这种方法简单直接,但是会有一个问题,存在严重的效率问题,很多的重复计算,因此,剑指Offer提出一种优化方法,使用内存记录之前的计算结果,从0 ,1 ,2 开始一直到n,计算过的值被记录下来,就不用再计算了。大大的节省了时间。

代码:Github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fibonacci(n) {
if (n === 0 || n === 1)
return n;
else {
var one = 0;
var two = 1;
var result = 0;
for (var i = 2; i <= n; i++) {
result = one + two;
one = two;
two = result
}
return result;
}

}

8、跳台阶

题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

思路:其实就是斐波那契函数的应用,如果n=1则只有一种,如果n=2则有两种,如果n=3,则有前两种之和,记一个函数f(n),则跳法为f(n-1)+f(n-2)

代码:Github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fibonacci(n) {
if (n === 1 || n === 2)
return n;
else {
var one = 0;
var two = 1;
var result = 0;
for (var i = 3; i <= n; i++) {
result = one + two;
one = two;
two = result
}
return result;
}

}

9、变态跳台阶

题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

思路:其实就是斐波那契函数的应用,如果n=1则只有一种,如果n=2则有两种,如果n=3,则有前两种之和在加上自己跳n阶的一种,记一个函数f(n),则跳法为f(n-1)+f(n-2)+…+f(1)+1

代码:Github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fibonacci(n) {
var arr = [1, 2];
if (n === 1 || n === 2)
return arr[n - 1];
else {
var result = 0;
for (var i = 3; i <= n; i++) {
result = 1 + arr.reduce(function (total, currentValue) {
return total + currentValue;
});
arr.push(result);
}
return result;
}

}


10、矩形覆盖

题目:我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

思路:其实就是斐波那契函数的应用,注意题目中是无重叠,如果n=1则只有一种,n=2时则有2种,
n=3时,左上角可以横着放也可以竖着放,竖着放的话,右边剩下两列,刚好是f(2)的情况,当横着放的时候,左下角必须横放一个,右边就剩下一列,f(1),则有f(2)+f(1)种,
因此推导f(n)= f(n-1)+f(n-2)

代码:Github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function rectCover(number) {
// write code here
var n = number;
var arr = [1, 2];
var result = 0;
if (n === 1 || n === 2)
return arr[n - 1];
else {
for (var i = 3; i <= n; i++) {
result = arr[arr.length - 1] + arr[arr.length - 2];
arr.push(result);
}
return result
}
}


11、二进制中1的个数

题目:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

思路

代码:Github


###

题目

思路

代码:Github


###

题目

思路

代码:Github

前端算法题.md

前段算法题汇总

前言:尝试自己根据思路去使用js实现的算法,发现对于算法有这么几个问题:

  • 算法是严谨的,一般都是设计到循环,所以要思考到循环中的特殊情况以及循环条件。
  • 不能钻牛角尖,无从下笔时,在纸上写思路,根据思路一步一步实现,在实现的过程中,如果遇到问题,不要慌忙推翻之前的思路,细想之后再改善,大的思路如果不是有很大的问题,先不要推翻,不然很容易陷入到取舍两难的境地。
  • 注意算法的调试方法,尤其是输出,断点的灵活应用。

1、数组去重

数组去重的几种方法请参照数组操作方法

2、js脚本整理文件

整理前:

1
2
3
4
111.232.213 ascqwdwd
111.232.213 qwdqwdqw
122.31.34.1 wdojqwodjqwp
232.34.13.3 adhwdhwqhd

整理后:

1
2
3
'111.232.213': [ 'ascqwdwd', 'qwdqwdqw' ],
'122.31.34.1': [ 'wdojqwodjqwp' ],
'232.34.13.3': [ 'adhwdhwqhd' ]

3、求1000以内所有的质数

对于求质数,和判断一个数是不是质数,原理是一样,区别在于求质数是对一群数进行判断,复杂度提高,如果只是判断一个数是不是质数,不要考虑复杂度,直接从2到这个数的平方根开始循环看能不能整除。
对于求多少以内的质数,也可以这样,但是这样的时间复杂度就提高了,网上有说几种不同的境界,这里就不介绍,我才用的筛选法,很明显,对于明显不是质数的数,没有做运算,节省的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/** 求num中所有质数
* 对比各种求质数的算法,试除法、筛选法两种时间复杂度较低,
* 本方法是自己优化的筛选法,因为2,3是比较小的两个质数,大部分数都是他们的倍数,明显不是质数,
* 所以使用2,3为基数,先筛选走这些非质数,然后在从5 7等向上筛选,这种方法明显循环次数要少。时间复杂度我也不知道该怎么计算。
* **/
function prime(num) {
//定义基数数组baseArr,用来存放较小的质数,res存放筛选结果,base存放[2,3]
let baseArr = [], res = [], base = [];
num = ~~num; //取整
//对2,3 特殊对待
if (num < 4) {
if (num === 1)
alert('请输入大于1的整数')
if (num === 2)
base.push(2);
else
base.push(2, 3)
}
//筛选剩下的
else {
baseArr.push(2, 3);
base.push(2, 3);
for (let i = 2; i <= num; i++) { //将2,3的倍数剔除,其余的放入res进行下一轮筛选
if (i % 2 !== 0 && i % 3 !== 0) {
res.push(i);
}
}
for (let j = 0; j <= baseArr.length - 1; j++) { //以大于3最小的质数开始筛选,
for (let i = 1; i < res.length; i++) {
if (res[i] % res[j] === 0 && i !== j) { //除自身外,其他的是质数的倍数的数被剔除
res.splice(i, 1);
}
}
if (res[j] < Math.sqrt(num)) { //确保基数质数肯定小于sqrt(num)
baseArr.push(res[j]); //添加一个新的基数质数。
}
}
}
return base.concat(res) //将特殊的[2,3]加入返回。
}
console.log(prime(100));
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
// baseArr = [2,3,5,7]

4、不借助临时变量,进行两个整数的交换

1
2
3
4
5
6
function exchange(a,b){
b=a-b;
a=a-b;
b=a+b;
return [a,b]
}

5、找出数组最大的差值

1
2
3
4
function maxDiff(array) {
return Math.max.apply(array,array) - Math.min.apply(array,array);
}
console.log(maxDiff(arr));

6、

JS中作用域与变量提升.md

ES5中for循环定义的全局变量

下面表达式输出的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
for(var i=1;i<4;i++){
setTimeout(function () {
console.log(i)
},0)
}
console.log(i)

//4 这是for循环执行完成之后,i++此时的i已经变成4,所以for循环之后的所有关于i的值都是4

//4
//4
//4

因为在表达式中定义的var i是全局作用域变量,任何一个地方都可以访问到,并且一定注意,在for循环内,如果都是同步执行的话,则i的值为3,但是如果是类似异步的,for执行完毕之后为i为4,所以所有i的值都为4.

立即执行函数

先看一段代码:

1
2
3
4
5
function (){ ... }();

//或者

function fName(){ ... }();

执行结果为: SyntaxError: Unexpected token (,为什么会有这个错误呢?

要理解两个概念

函数声明

1
2
3
function name([param[, param[, ... param]]]) {
statements
}

函数表达式

1
//一个匿名函数的函数表达式,被赋值给变量f2:
1
2
3
4
5
6
7
var f2 = function() {
console.log("f2");
}

//通过()来调用此函数

f2();

所以我们来看之前报错的原因,这是一个函数声明,后面再接一个() ,js引擎看到function关键字之后,认为后面跟的是函数定义语句,不应该以圆括号结尾,解决这个问题就是让引擎知道,圆括号前面的部分不是函数定义语句,而是一个表达式,可以对此进行运算。改写成这样。

1
2
3
4
5
(function(){ /* code */ }()); 

// 或者

(function(){ /* code */ })();
带参数(parameter)

有时候看到这样的一种形式,括号中传递一个参数。

1
(function (p) { return p})(parameter);

立即 执行的括号中传入了一个参数,执行的过程中此参数就是函数传入的参数。

JS事件.md

事件流

事件代理

事件绑定后,检测顺序就会从被绑定的DOM下滑到触发的元素,再冒泡会绑定的DOM上。也就是说,如果你监听了一个DOM节点,那也就等于你监听了其所有的后代节点。

代理的意思就是只监听父节点的事件触发,以来代理对其后代节点的监听,而你需要做的只是通过currentTarget属性得到触发元素并作出回应。

使用事件代理意味着你可以节省大量重复的事件监听,以减少浏览器资源消耗。还有一个好处就是让HTML独立起来,比如之后还有要加子元素的需求,也不需要再为其单独加事件监听了。

事件处理程序

DOM0级事件处理就是讲一个函数赋值给一个事件处理程序属性

1
btn.onclick=function (){}

DOM2级事件处理程序,这里因为历史原因,IE的事件处理操作API名称和常规的不太一样,所以,一般需要考虑到浏览器兼容,高程上给出了一个扩浏览器的EventUtil对象,会检测是是否支持DOM2级事件处理,以及IE下的情况。优先推荐。
注意 :addEventListener()/removeEventListener()最后一个参数为boolean值,true表示在捕获阶段调用事件处理函数,false表示在冒泡阶段处理。

IE下的attachEvent() detachEvent()没有这个布尔值,因为早期IE只支持冒泡,所以IE处理默认冒泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/** DOM2级事件处理程序  浏览器兼容
* IE-attachEvent() detachEvent()
* 其他-addEventListener() removeEventListener()
* EventUtil: 全局对象
* 使用:EventUtil.addHandler(ele,type,handler)
* **/
let EventUtil = {
addHandler: (element, type, handler) => {
if (element.addEventListener) {
element.addEventListener(type, handler, false)
} else if (element.attachEvent) {
element.attachEvent('on' + type, handler)
} else {
element['on' + type] = handler;
}
},
removeHandler: (element, type, handler) => {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false)
} else if (element.detachEvent()) {
element.detachEvent('on' + type, handler)
} else {
element['on' + type] = null;
}
}
}

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event ,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有浏览器都支持 event 对象,但支持方式不同。

当然,在事件对象中IE也有不一样的地方。后面再区别,先看DOM2级事件对象。

在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则 this 、 currentTarget 和 target 包含相同的值。。如果事件处理程序存在于按钮的父节点中(例如 document.body ),那么这些值是不相同的。

IE中的事件对象

事件类型

这里面主要记住UI事件,大部分是window对象相关。

内存和性能优化

在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM访问次数,会延迟整个页面的交互就绪时间。当然最直接的就是减少页面中事件处理程序

优化方案一:使用事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。一个典型的应用就是比如有一批列表,给每一个列表都添加一个事件处理程序,则内存占用比较大,然后操作也比较复杂,如果在DOM中尽量高一层级上添加一个事件处理程序,利用事件冒泡,可以让每一个列表都可以获取到事件处理。这种技术对用户最终的结果与之前的方法差不多,但是需要的内存更少。

使用事件委托技术,与传统方法相比较,有如下优势:

优化方案二:移除时间事件处理程序

内存中保留着那些过时不用的“空事件处理程序”,也是造成Web应用程序内存和性能问题的主要原因。两种典型的情况会造成“空事件处理程序占用内存”的问题:

  • 从文档中移除带有事件处理程序的元素时。
    这可能是通过纯粹的 DOM 操作,例如使用 removeChild() 和 replaceChild() 方法,但更多地是发
    生在使用 innerHTML 替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML 删除
    了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。

一般使用手工移除事件处理程序,btn.onclick=null;,然后再设置innerHTML。

  • 卸载页面。如果在页面被卸载之前没
    有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面再卸载页面时(可能是在两个页
    面间来回切换,也可以是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序
    占用的内存并没有被释放。

一般来说,最好的做法是在页面卸载之前,先通过onunload事件处理程序移除所有事件处理程序。

说到 这里,就又可以看到事件委托技术的优势,如果事件追踪的越少,越容易移除。

mouse事件

mouseover与mouseenter

不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件。

只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件。

mouseout与mouseleave

不论鼠标指针离开被选元素还是任何子元素,都会触发 mouseout 事件。

只有在鼠标指针离开被选元素时,才会触发 mouseleave 事件。

递归.md

递归

递归函数是在一个函数通过名字调用自身的情况下构成的。

1
2
3
4
5
6
7
function factorial(num){ 
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}

这是一个经典的递归阶乘函数。虽然这个函数表面看来没什么问题,但下面的代码却可能导致它出错。

1
2
3
var anotherFactorial = factorial; 
factorial = null;
alert(anotherFactorial(4)); //出错!

原始函数中有对factorial的引用,将factorial=null后factorial被销毁了,所以执行这个引用后找不到。

使用arguments.callee

稳妥的递归用法使用arguments.calleearguments是一个对应于传递给函数的参数的类数组对象,arguments.callee指向正在执行的函数指针。

1
2
3
4
5
6
7
function factorial(num){ 
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}

但是在严格模式下,不能访问arguments.callee ,使用命名函数表达式实现。

1
2
3
4
5
6
7
var factorial = (function f(num){ 
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});

以上代码创建了一个名为 f() 的命名函数表达式,然后将它赋值给变量 factorial 。即便把函数赋值给了另一个变量,函数的名字 f 仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。

正则表达式.md

关于正则表达式的匹配,一开始觉得很复杂,主要是因为表达式看不懂,不知道什么是匹配符,限定符,操作符,其实使用过后才发现,正则表达其实是比较简单的,具体的参考下面一篇博文,《JavaScript学习总结(八)正则表达式》里面对于概念的讲解比较详细,正则的使用主要看自己怎么去组合,想要匹配什么样的内容,很多时候,我们要用到的正则匹配,其实网上都有比较全的正则表达式,直接搜索就可以获取到。

充分利用网上的资源才是正确学习正则的途径。

我对于正则的害怕主要是因为记不住正则表达式中特殊字符,MDN有一张表格,很完整很权威,对照这张表格就可以的。要求看到特殊字符就知道是什么意思。

MDN-正则表达式

注意正则有一些字符类匹配,很多时候大家都是使用字符类匹配。这个要记住。

重复匹配符也要注意,这个也要记住,一看就能知道表达的匹配范围。

其他的东西就直接在博文上看就可以了。

下面有一个导图,记住这个导图你就理解了。

日期格式化.md

日期格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
'usestrict';

/**=========================================================
* 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
* 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* 例子:
* (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
* (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
=========================================================*/

(function () {
Date.prototype.format = function (fmt) {
if (typeof(fmt) == "undefined") {
fmt = "yyyy-MM-dd hh:mm:ss";
}
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
//年份是最多表示有四位数,所以分开来处理
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
//遍历其他的都是两位数的时间内容
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt))
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ?
(o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}

return fmt;
};

Date.prototype.formatDay = function () {
return this.format("yyyy-MM-dd");
};

Date.prototype.formatDay2 = function () {
return this.format("yyyyMMddhhmmss");
};

})();

关于这个时间格式化代码,这段厉害就厉害在于,正则表达式应用的非常炉火纯青。尤其replace那一段,使用捕获括号匹配字符串并记住字符串,用$1表示记住的第一个匹配对象,后面替换也很巧妙,设定好位数,然后字符串拼接,最重要的一步,再使用substr截取字符串,一般情况下我想的就是拼凑,先删都拼,比较好的做法是先拼后删。

数组排序算法.md

前言

以前对于排序,只知道冒泡排序,了解一点快速排序,一直是我的弱点,今天是时候好好攻克一下了
参考:JS中可能用得到的全部的排序算法

时间复杂度

1、冒泡排序

冒泡排序需要两个嵌套的循环. 其中, 外层循环移动游标; 内层循环遍历游标及之后(或之前)的元素, 通过两两交换的方式, 每次只确保该内循环结束位置排序正确, 然后内层循环周期结束, 交由外层循环往后(或前)移动游标, 随即开始下一轮内层循环, 以此类推, 直至循环结束.

1
2
3
4
5
6
7
// 交换下标a、b对应的数组数值,此方法抽离处理,后面直接使用,不做说明
function exchange(a, b, obj) {
let temp = obj[a];
obj[a] = obj[b];
obj[b] = temp;
// return obj
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function sort(array) {
let count=0;
for (let i = 0, len = array.length; i < len; i++) {
for (let j = 0; j < len; j++) {
if (array[i] < array[j]) {
count++
exchange(i, j, array)
}
}
}
//count=81
return array;
}

2、双向冒泡排序

双向冒泡排序是冒泡排序的一个简易升级版, 又称鸡尾酒排序. 冒泡排序是从低到高(或者从高到低)单向排序, 双向冒泡排序顾名思义就是从两个方向分别排序(通常, 先从低到高, 然后从高到低). 因此它比冒泡排序性能稍好一些.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function sort(array) {
for (let i = 0, len = array.length; i < len; i++) {
for (let j = 0; j < len; j++) {
if (array[i] < array[j]) {
exchange(i, j, array)
}
}
for (let k = len - 1; k >= 0; k--) { //从一个方向开始循环检查
if (array[i] < array[k]){

exchange(i, k, array)
}
}
}
// count=81
return array;
}

3、选择排序

从算法逻辑上看, 选择排序是一种简单且直观的排序算法. 它也是两层循环. 内层循环就像工人一样, 它是真正做事情的, 内层循环每执行一遍, 将选出本次待排序的元素中最小(或最大)的一个, 存放在数组的起始位置. 而 外层循环则像老板一样, 它告诉内层循环你需要不停的工作, 直到工作完成(也就是全部的元素排序完成).
Tips
: 选择排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序. 比如数组[2,2,1,3], 正向排序时, 第一个数字2将与数字1交换, 那么两个数字2之间的顺序将和原来的顺序不一致,虽然它们的值相同, 但它们相对的顺序却发生了变化. 我们将这种现象称作不稳定性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 3、选择排序 **/
function sort(array) {
let res = [], min;
for (let i = 0, len = array.length; i < len - 1; i++) { //由于第一个开始跟后面对比,因此只需要对比n-1
min = i;
for (let j = i + 1; j < len; j++) {
if (array[min] > array[j]) {
min = j //每一轮循环记下此轮最小值的下标,在后面将此下标值放到数列前面
}
}
exchange(min, i, array)
}
return array;
}

插入排序

插入排序的设计初衷是往有序的数组中快速插入一个新的元素. 它的算法思想是: 把要排序的数组分为了两个部分, 一部分是数组的全部元素(除去待插入的元素), 另一部分是待插入的元素; 先将第一部分排序完成, 然后再插入这个元素. 其中第一部分的排序也是通过再次拆分为两部分来进行的.

插入排序由于操作不尽相同, 可分为 直接插入排序 , 折半插入排序(又称二分插入排序), 链表插入排序 , 希尔排序 .

4、直接插入排序

它的基本思想是: 将待排序的元素按照大小顺序, 依次插入到一个已经排好序的数组之中, 直到所有的元素都插入进去.
由于直接插入排序每次只移动一个元素的位置, 并不会改变值相同的元素之间的排序, 因此它是一种稳定排序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 4、直接插入排序 **/
function sort(array) {
for (let i = 1, len = array.length; i < len; i++) { //直接从第二个开始比较
let current = array[i]; //将这次要比较的值存储起来,作为这一轮循环里面的常量
let index = i;

//从index前一位数开始比较比较,如果当前比较的数小于它的前一位,则将大的数后移一位
while (current < array[index - 1] && index >= 0) {
array[index] = array[index - 1];
index--; //于此同时,下标也前移一位,继续下一次比较
}
if (index !== i) { //防止自己赋值给自己,
array[index] = current //如果不是自己,则一定是之前后移空出来留给current的位置,插入进去
}
}
return array;
}

5、折半插入排序

折半插入排序是直接插入排序的升级版. 鉴于插入排序第一部分为已排好序的数组, 我们不必按顺序依次寻找插入点, 只需比较它们的中间值与待插入元素的大小即可.

说白一点就是从第二个数开始向前比较,前面的数(已经排好序)分成两半,找到中间的数,比较大小,然后在向中间循环,一直找到介于两者之间的,然后把之后的后移,将此值插入到后移留出来的位置。

和直接插入法相比,少了一半数据量的对比。

算法基本思想是:

  1. 取0 ~ i-1的中间点( m = (i-1)>>1 ), array[i] 与 array[m] 进行比较, 若array[i] < array[m] , 则说明待插入的元素array[i] 应该处于数组的 0 ~ m 索引之间; 反之, 则说明它应该处于数组的 m ~ i-1 索引之间.
  2. 重复步骤1, 每次缩小一半的查找范围, 直至找到插入的位置.
  3. 将数组中插入位置之后的元素全部后移一位.
  4. 在指定位置插入第 i 个元素.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/** 5、折半插入排序 **/
function sort(array) {
for (let i = 1, len = array.length; i < len; i++) { //直接从第二个开始比较 i=1
let low = 0, high = i - 1, current = array[i];
//找中值对应的下标
while (low <= high) {
// let m = Math.floor((low+high) / 2);
let m = (low + high) >> 1; //x>>1 是位运算中的右移运算, 表示右移一位, 等同于x除以2再取整, 即 x>>1 == Math.floor(x/2) .
if (array[i] <= array[m]) {
high = m - 1 //保证low<=high顺利成立,将high定位低半区
} else {
low = m + 1 //同理
}
}

//将在要插入值位置之后的所有元素后移,留出插入的位置
for (let j = i; j > low; j--) {
array[j] = array[j - 1];
console.log(array)
}
array[low] = current //插入

}
return array;
}

6、希尔排序

希尔排序也称缩小增量排序, 它是直接插入排序的另外一个升级版, 实质就是分组插入排序. 希尔排序以其设计者希尔(Donald Shell)的名字命名, 并于1959年公布.

算法的基本思想:

  1. 将数组拆分为若干个子分组, 每个分组由相距一定”增量”的元素组成. 比方说将[0,1,2,3,4,5,6,7,8,9,10]的数组拆分为”增量”为5的分组, 那么子分组分别为 [0,5], [1,6], [2,7], [3,8], [4,9] 和 [5,10].
  2. 然后对每个子分组应用直接插入排序.
  3. 逐步减小”增量”, 重复步骤1,2.
  4. 直至”增量”为1, 这是最后一个排序, 此时的排序, 也就是对全数组进行直接插入排序.

示意图:

可见, 希尔排序实际上就是不断的进行直接插入排序, 分组是为了先将局部元素有序化. 因为直接插入排序在元素基本有序的状态下, 效率非常高. 而希尔排序呢, 通过先分组后排序的方式, 制造了直接插入排序高效运行的场景. 因此希尔排序效率更高.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/** 6、希尔排序 **/
//封装直接插入排序
function dirSort(array, grap) {
grap = arguments[1] ? grap : 1; //如果grap传值了,则为grap传入的值,如果没有,则从1开始
// debugger;
for (let i = grap - 1, len = array.length; i < len; i++) { //直接从第grap-1个开始比较
let current = array[i]; //将这次要比较的值存储起来,作为这一轮循环里面的常量
let index = i;

//只比较分组内的最大步长间距的两个数,循环一次就跳出来
while (current < array[index - grap] && index >= 0) {
array[index] = array[index - grap]; //如果当前比较的数小于它的前一位,则将大的数后移一位
index -= grap; //于此同时,下标也前移grap,继续下一次比较
}


if (index !== i) { //防止自己赋值给自己,
array[index] = current; //如果不是自己,则一定是之前后移空出来留给current的位置,插入进去
console.log(array)
// debugger
}
}
return array;
}

function sort(array) {
let gra = array.length >> 1;
while (gra > 0) {
dirSort(array, gra);
gra = gra >> 1;
}
return array
}

7、快速排序

这是一种使用非常广泛的排序,典型的代表就是Chrome的v8引擎为了高效排序, 在排序数据超过了10条时, 便会采用快速排序. 对于10条及以下的数据采用的便是插入排序

快速排序”的思想很简单,整个排序过程只需要三步:
(1)在数据集之中,选择一个元素作为”基准”(pivot)。
(2)所有小于”基准”的元素,都移到”基准”的左边;所有大于”基准”的元素,都移到”基准”的右边。
(3)对”基准”左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** 7、快速排序 **/
function sort(array) {
if (array.length >= 1) {
//选取基准点,基准点选取的好坏决定了排序速度的好坏,目前没有好的方法,一般居中选取
let baseIndex = array.length >> 1;
let left = [], right = []; //定义基准点左右两边数组,小的都在左边,大的都在右边
// 循环判断
for (let i = 0; i < array.length; i++) {
if (i !== baseIndex) { //这一步很关键,一定不能把自身算进去
if (array[i] < array[baseIndex]) {
left.push(array[i])
} else
right.push(array[i])
}
}
// 最关键的一步:递归调用然后在用数组拼接
return arguments.callee(left).concat(array[baseIndex], arguments.callee(right))
}
else
return array;
}

8、归并排序

归并排序建立在归并操作之上, 它采取分而治之的思想, 将数组拆分为两个子数组, 分别排序, 最后才将两个子数组合并; 拆分的两个子数组, 再继续递归拆分为更小的子数组, 进而分别排序, 直到数组长度为1, 直接返回该数组为止.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 8、归并排序 **/
function sort(array) {
if (array.length < 2)
return array;
else {
let res = [];
//上半部分拆分数组,
let baseIndex = array.length >> 1;
let left = array.slice(0, baseIndex),
right = array.slice(baseIndex);

//合并数组,
function merge(left, right) {
while (left.length && right.length) {
let item = left[0] <= right[0] ? left.shift() : right.shift();
res.push(item);
}
return res.concat(left.length ? left : right); //返回排好数组
}

//递归调用,一边拆分,一边合并
return merge(arguments.callee(left), arguments.callee(right));
}
}

9、堆排序

10、计数排序

计数排序利用了一个特性, 对于数组的某个元素, 一旦知道了有多少个其它元素比它小(假设为m个), 那么就可以确定出该元素的正确位置(第m+1位)

  1. 获取待排序数组A的最大值, 最小值.
  2. 将最大值与最小值的差值+1作为长度新建计数数组B,并将相同元素的数量作为值存入计数数组.
  3. 对计数数组B累加计数, 存储不同值的初始下标.
  4. 从原数组A挨个取值, 赋值给一个新的数组C相应的下标, 最终返回数组C.

11、桶排序

桶排序即所谓的箱排序, 它是将数组分配到有限数量的桶子里. 每个桶里再各自排序(因此有可能使用别的排序算法或以递归方式继续桶排序). 当每个桶里的元素个数趋于一致时, 桶排序只需花费O(n)的时间. 桶排序通过空间换时间的方式提高了效率, 因此它需要额外的存储空间(即桶的空间).

12、基数排序

基数排序源于老式穿孔机, 排序器每次只能看到一个列. 它是基于元素值的每个位上的字符来排序的. 对于数字而言就是分别基于个位, 十位, 百位 或千位等等数字来排序. (不明白不要紧, 我也不懂, 请接着往下读)