前端必会算法(原理及代码实现)
推荐书籍:算法导论
一维数据结构:线性数据结构(数组,链表),线性的数据结构强调存储与顺序
线性数据结构之数组
1.存储在物理空间上是连续的,这个与操作系统的内存分配有关
2.底层的数据长度是不可变化的。当长度发生变化时,数组需要扩容,这个会消耗内存(过程中涉及新数据的创建和数组数据的复制,注意,数组的创建会多扩充几位,防止经常扩容,如8位数据扩充为16位数组);在js语言中会发现数组长度是可以变化的,是因为js引擎内部做了优化,帮用户做了扩容。
3.数组的变量,指向了数组第一个元素的位置
4.数组a[1]中的方括号表示存储地址的偏移。操作系统小知识:通过偏移查询数据性能最好。
优点:
1.查询性能好,可以指定查询某个位置
缺点:
1.因为空间必须是连续的,所以如果数组比较大,当系统的空间碎片较多的时候,容易存储不下。 空间碎片较时,连续的内存空间可能就没有了,导致明明空间足够,而大数组却存储不下;
2.因为数组的长度是固定的,所以数组的内容难以被添加和删除。 ---添加的代价(可能扩容和复制);删除的代价(数据往前移动)
线性数据结构之链表(默认单链表,还有一些如双链表等)
链表特性:
1.空间上不是连续的。
2、每存放一个值,都有开销一个引用空间。
3.传递一个链表时,必须传递链表的根节点。(每一个节点都认为自己是根节点。)
优点:
1.只要内存足够大,就能存的下,不用担心空间碎片的问题
2.链表的添加和删除非常容易
缺点:
1.查询速度慢(指的查询某个位置)
2.链表每一个节点都需要创建一个指向next的引用,浪费空间,但是当节点内的数据越多的时候,这部分多开销的内存影响越少
定义链表:function Node(value){this.value=value;this.next=null}
定义双向链表链表:function Node(value){this.value=value;this.next=null}
线性数据结构的遍历
数组遍历略
链表遍历(循环遍历和递归遍历)
function bianLink(root){
var temp = root;
while(true){
if(temp != null){
console.log(temp.value);
}else{
break;
}
temp = temp.next;
}
}
递归遍历,必须要有出口
function bianLink(root){
if(root == null) return;
console.log(root.value);
bianLink(root.next);
}
链表的逆置(算法中最简单的题目,算法基础)
突破口:找到倒数第二个节点(并非找到最后一个节点,因为每一个节点都认为自己是根节点,无法找到倒数第二个节点了,因此不能进行倒置)
function nizhi(root){
if(root.next == null){
return root; //函数的出口
}else{
return nizhi(root.next);
}
}
虽然找到了最后一个,但是没有用,找不到倒数第二个节点了,因此要找到倒数第二个节点root.next.next
function nizhi(root){ //参数为初始节点
if(root.next.next == null){ //表示当前的节点是倒数第二个节点
root.next.next = root; //让最后一个节点指向自己(倒数第二个节点)
return root.next; //返回最后一个节点
}else{ //如果不是倒数第二个
let result = nizhi(root.next); //先拿到当前结果,进行逆置,最后放回结果
root.next.next = root; //将自己的下一个节点指向自己
root.next = null; //将自己的next指向null,放在来回连接;主要是将第一个的节点next指向null
return result;
}
}
例子:
let newRoot = nizhi(node1);
bianlink(newRoot);
七种常见的数组排序方法
数组排序
冒泡排序(数组排序)
1利用原生数组中的sort方法(冒泡原理)
arr.sort((a,b)=>{return a-b}) //升序排序
//性能一般,复杂度为n*(n-1)
var arr=[1,5,7,9,16,2,4];
//冒泡排序,每一趟找出最大的,总共比较次数为arr.length-1次,每次的比较次数为arr.length-1次,依次递减
var temp;
for(var i=0;i<arr.length-1;i++){
for(var j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
选择排序(假定某个位置的值是最小值)
//性能一般
var arr=[1,23,5,8,11,78,45];
var temp;
for(var i=0;i<arr.length-1;i++){
for(var j=i+1;j<arr.length;j++){
if(arr[i]>arr[j]){ //默认选择第一项
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
}
快速排序(一拆为二) ---简单快排
快排的算法思路:一分为二,递归
//会创建很多的数组
function quickSort(arr){
if(arr.length <= 1){ //算法严谨性,性能优化,小于1也是可以的,递归出口
return arr;
}
var left = [];
var right = [];
var midIndex = parseInt(arr.length / 2); //数组一分为二,默认中间的为leader,大家和它比
var mid = arr[midIndex];
for(var i = 0 ; i < arr.length ; i++){
if(i == midIndex) continue;
if( arr[i] < mid){
left.push(arr[i])
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat([mid],quickSort(right)); //递归调用
}
快速排序(一拆为二) ---标准快排(begin,end通常都是指左闭右开区间)
快排的算法思路:一分为二,递归
一、默认比较值为数组最后一位,左右扫描,找到左边第一个大于k的,找到右边第一个小于k的,两者交换
function quicksort(arr,low, high) {
if (low >= high) { //出口
return;
}
int k = arr[high];
int left = low;
int right = high-1;
while (left < right) {
while (arr[left] < k && left < right) {
++left;
}
while (arr[right] >= k && left < right) {
--right;
}
swap(arr,left, right); //交换
}
if (arr[left] < k) {
++left;
}
swap(arr,left, high);
quicksort(arr, low, left-1);
quicksort(arr, left+1, high);
}
辅助函数:swap
function swap(arr,a,b){
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
注意:
1. left左边(不包括left)为<key的数字,right右边(不包括right)为>=key的数字
2. 当退出循环时,left = right, 此时无法确定arr[left]和k谁大,需进行判定。如果arr[left] < k, 则partition位置为left+1, 否则为left
3. 为了避免partition位置位于左边界或右边界,即mid == low || mid == high, 在函数入口处需要对low和high进行校验
二、左中扫描
int partition(int arr[], int low, int high) {
if (low >= high) {
return;
}
int k = arr[high];
int left = low-1;
int mid = low;
for (; mid <= high-1; ++mid) {
if (arr[mid] <= k) {
left++;
std::swap(arr[left], arr[mid]);
}
}
std::swap(arr[left+1], arr[mid]);
return left+1;
}
void quicksort(int arr[], int low, int high) {
if (low >= high) {
return;
}
int mid = partition(arr, low, high);
partition(arr, low, mid-1);
partition(arr, mid+1, high);
}
注意:
1. left左边(包括low)为<=k的数字, 所以partition位置为left+1
其他同上
其他较不常用算法
1.插入排序法:将要排序的数组分成两部分,每次从后面的部分取出索引最小的元素插入到前一部分的适当位置
var arr=[45,1,32,21,56,87,43,12,34,45];
for(var i=0;i<arr.length;i++){
var n=i;
while(arr[n]>arr[n+1] && n>=0){
var temp=arr[n];
arr[n]=arr[n+1];
arr[n+1]=temp;
n--;
}
}
-
希尔排序(性能最好的排序)
function xier(arr){ var interval = parseInt(arr.length / 2); //分组间隔设置 while(interval > 0){ for(var i = 0 ; i < arr.length ; i ++){ var n = i; while(arr[n] < arr[n - interval] && n > 0){ var temp = arr[n]; arr[n] = arr[n - interval]; arr[n - interval] = temp; n = n - interval; } } interval = parseInt(interval / 2); } return arr; } xier([12,9,38,44,7,98,35,59,49,88,38]);
栈和队列
数组封装一个栈结构
function Stack(){ this.arr = []; this.push = function(value){ this.arr.push(value); } this.pop= function(){ return this.arr.pop(); 改为队列结构,this.arr.shift() } } var stack = new Stack() var queue = new Queue()
二维数据结构
二维数组,树形结构
二维数组略
树形结构
树形结构有一个根节点,树形结构没有回路,是有向无环图,树是图的一种
关于树的概念:
1.有一个根节点
2.叶子节点:下边没有其他节点了
3.节点:既不是根节点,又不是叶子节点
4.树的度:这棵树有最多分叉的节点有多少个分叉,这棵树的度就为多少
5.树的深度:树最深有几层,树的深度就为多少。
6.子节点:某个节点下的节点
7.父节点:上级节点
二叉树
树的度最多为2的树形结构
二叉树之满二叉树
概念:(1)所有的叶子节点都在最底层
(2)每一个节点(非叶子节点)都有两个子节点
二叉树之完全二叉树
国内概念:(1)叶子节点都在最后一层或者倒数第二层
(2)如果有叶子节点,叶子节点都向左聚拢
国际概念:(1)叶子节点都在最后一层或者倒数第二层
(2)如果有叶子节点,就必然有两个叶子节点
二叉树中,子树的概念
概念:1.二叉树中,每一个节点或者叶子节点,都是一颗子树的根节点
2.左子树、右子树
二叉树的遍历(几乎校招每年的必考题)
考点:
-
给出二叉树,写成前序中序后序遍历
-
写成前序中序后序遍历的代码
-
给出前序中序还原二叉树,要求写出后序遍历(中序一定要有)
-
给出后序中序还原二叉树,要求写出前序遍历(中序一定要有)
-
代码实现前序中序还原二叉树
-
代码实现中序后序还原二叉树
传递二叉树要传递根节点。
前序遍历(先根次序遍历):先打印当前的(中间的),再打印左边的,最后打印右边的
中序遍历(中根次序遍历):先打印左边的,再打印当前的,最后打印右边的(中序遍历其实就是从左到右进行投影)
后序遍历(后根次序遍历):先打印左边的,再打印右边的,最后打印当前的
打印顺序:前序遍历根节点在最左边,中序遍历根节点在中间,后序遍历根节点在最右边
代码实现前中后序
节点创建 function Node(value){ this.value = value ; this.left = null; this.right = null; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); var f = new Node("f"); var g = new Node("g"); a.left = c; a.right = b; c.left = f; c.right = g; b.left = d; b.right = e;
前序遍历(用的最多的树遍历)
function f1(root){ if(root == null) return; console.log(root.value); f1(root.left); f1(root.right); } f1(root);
中序遍历
function f1(root){ if(root == null) return; f1(root.left); console.log(root.value); f1(root.right); } f1(root);
后序遍历
function f1(root){ if(root == null) return; f1(root.left); f1(root.right); console.log(root.value); } f1(root);
根据前序中序还原二叉树
前序遍历:ACFGBDE
中序遍历:FCGADBE
得出后序遍历:FGCDEBA
根据后序中序还原二叉树
中序遍历:FCGADBE
后序遍历:FGCDEBA
得出前序遍历:ACFGBDE
代码实现前序中序还原二叉树
不能默认为就是满二叉树,要得出普通二叉树,必须知道中序以及前序(或者后序)
var qian = ['a', 'c', 'f', 'g', 'b', 'd', 'e'] var zhong = ['f', 'c', 'g', 'a', 'd', 'b', 'e'] function Node(value){ this.value = value ; this.left = null; this.right = null; } function f1(qian,zhong){ if(qian == null || zhong == null || qian.length == 0 || zhong.length == 0 || qian.length != zhong.length) { return null;} var root = new Node(qian[0]); //创建根节点|第二次就为节点 var index = zhong.indexOf(root.value);//找到根节点在中序遍历中的位置 var qianLeft = qian.silce(1,1+index); //前序遍历的左子树 var qianRight = qian.silce(index+1,qian.length); //前序遍历的右子树 var zhongLeft = zhong.silce(0,index); //中序遍历的左子树 var zhongRight = zhong.silce(index+1,zhong.length);//中序遍历的右子树 root.left = f1(qianLeft,zhongLeft); //根据左子树的前序和中序还原左子树并赋值给root.left root.right = f1(qianRight,zhongRight);//根据右子树的前序和中序还原右子树并赋值给root.right return root; } var root = f1(qian,zhong);
代码实现中序后序还原二叉树
不能默认为就是满二叉树,要得出普通二叉树,必须知道中序以及前序(或者后序)
var zhong = ['f', 'c', 'g', 'a', 'd', 'b', 'e'] var hou = ['f', 'g', 'c', 'd', 'e', 'b', 'a'] function Node(value){ this.value = value ; this.left = null; this.right = null; } function f1(zhong,hou){ if(hou == null || zhong == null || hou.length == 0 || zhong.length == 0 || hou.length != zhong.length) { return null;} var root = new Node(hou[hou.length]); //创建根节点|第二次就为节点 var index = zhong.indexOf(root.value);//找到根节点在中序遍历中的位置 var zhongLeft = zhong.silce(0,index); //中序遍历的左子树 var zhongRight = zhong.silce(index+1,zhong.length);//中序遍历的右子树 var houLeft = hou.silce(0,index); //后序遍历的左子树 var houRight = hou.silce(index,hou.length-1); //后序遍历的右子树 root.left = f1(zhongLeft,houLeft); //根据左子树的中序和后序还原左子树并赋值给root.left root.right = f1(zhongRight,houRight);//根据右子树的中序和后序还原右子树并赋值给root.right return root; } var root = f1(zhong,hou);
二叉树的深度搜索和广度搜索(重点)
深度优先搜索:更适合探索未知
广度优先搜索:更适合探索局域
代码实现二叉树的深度优先搜索
节点创建 function Node(value){ this.value = value ; this.left = null; this.right = null; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); var f = new Node("f"); var g = new Node("g"); a.left = c; a.right = b; c.left = f; c.right = g; b.left = d; b.right = e; //对于二叉树来说,深度优先搜索,和前序遍历的顺序是一样的 function deepSearch(root,target){ if(root == null){ return false; } if(root.value == target.value){ return true; } var left = deepSearch(root.left,target); var right = deepSearch(root.right,target); return left||right; } console.log(deepSearch(a,"f"));
代码实现二叉树的广度优先搜索(较难)
// 节点创建 function Node(value) { this.value = value; this.left = null; this.right = null; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); var f = new Node("f"); var g = new Node("g"); a.left = c; a.right = b; c.left = f; c.right = g; b.left = d; b.right = e; //对于二叉树来说,广度优先搜索 function f1(rootList, target) { if (rootList == null || rootList.length == 0) { return false; } var childList = []; //当前层所有节点的子节点,都在这个list中,这样传入下一层级的时候 for (i = 0; i < rootList.length; i++) { //rootList[i]为null的时候怎么处理 // console.log(rootList[i], `第${i}遍`); if (rootList[i] != null && rootList[i].value == target) { return true; } else { //rootList[i]的左右节点为null的时候怎么处理,若为空,当为空时不处理。即不push if (rootList[i].left != null) { childList.push(rootList[i].left); } if (rootList[i].right != null) { childList.push(rootList[i].right); } } } // console.log(childList); return f1(childList, target); } console.log(f1([a], "f"));
二叉树的比较
// 节点创建 function Node(value) { this.value = value; this.left = null; this.right = null; } //第一棵树 var a1 = new Node("a"); var b1 = new Node("b"); var c1 = new Node("c"); var d1 = new Node("d"); var e1 = new Node("e"); var f1 = new Node("f"); var g1 = new Node("g"); a1.left = c1; a1.right = b1; c1.left = f1; c1.right = g1; b1.left = d1; b1.right = e1; //第二棵树 var a2 = new Node("a"); var b2 = new Node("b"); var c2 = new Node("c"); var d2 = new Node("d"); var e2 = new Node("e"); var f2 = new Node("f"); var g2 = new Node("g"); a2.left = c2; a2.right = b2; c2.left = f2; c2.right = g2; b2.left = d2; b2.right = e2; //左右子树交换后,不是同一颗树,这里是左子树和左子树比,右子树和右子树比 function compareTree(root1, root2) { // root1和root2都为空算相等吗?? if (root1 == root2) { //两个树对应同一个地址 return true; } if ((root1 == null && root2 != null) || (root2 == null && root1 != null)) { //其中一个为空,另一个不为空 return false; } if (root1.value != root2.value) { //相同位置的值不相等 return false; } else { //当前值相等 var leftBool = compareTree(root1.left, root2.left); //判断左子树是否相等 var rightBool = compareTree(root1.right, root2.right); //判断右子树是否相等 return leftBool && rightBool; //必须左右子树都相等才算相等 } } console.log(compareTree(a1, a2));
注意:遇到二叉树比较的问题时,必须要确定,左右两个子树在交换位置后,是否还算一颗二叉树。
如果是笔试,默认只有呼唤后不是同一棵树;如果是面试,可以问一下。
二叉树左右子树允许互换的比较
//左右子树可以交换的情况 function compareTree(root1, root2) { // root1和root2都为空算相等吗?? if (root1 == root2) { //两个树对应同一个地址 return true; } if ((root1 == null && root2 != null) || (root2 == null && root1 != null)) { //其中一个为空,另一个不为空 return false; } if (root1.value != root2.value) { //相同位置的值不相等 return false; } else { //当前值相等 return compareTree(root1.left, root2.left) && compareTree(root1.right, root2.right) || compareTree(root1.left, root2.right) && compareTree(root1.right, root2.left) } }
二叉树的diff算法
两棵树的比较:新增,修改,删除什么?
最小生成树(一个图生成最小生成树)
表示一个图,可以使用点集合和边集合
点集合:[a,b,c,d,e,f]
A | B | C | D | E | |
---|---|---|---|---|---|
A | 0 | 4 | 7 | max | max |
B | 4 | 0 | 8 | 6 | max |
C | 7 | 8 | 0 | 5 | max |
D | Max | 6 | 5 | 0 | 7 |
E | max | Max | max | 7 | 0 |
比如:需要将所有的村庄联通,但需要花费最少的钱?
普利姆算法(加点法)
-
任选一个点作为起点
-
找到以当前选中点为起点路径最短的边
-
如果这个边的另一端没有被联通进来,那么就连结
-
如果这个边的另一端也早就被连进来了,则看倒数第二短的边
-
重复2-4步骤直到将所有的点都联通为止。
普利姆算法代码实现
var max = 1000000; var pointSet = []; var distance = [ [0, 4, 7, max, max], [4, 0, 8, 6, max], [7, 8, 0, 5, max], [max, 6, 5, 0, 7], [max, max, max, 7, 0] ] function Node(value) { this.value = value; this.neighbor = []; //图的一个节点可能有很多节点 } var a = new Node("A"); var b = new Node("B"); var c = new Node("C"); var d = new Node("D"); var e = new Node("E"); pointSet.push(a); pointSet.push(b); pointSet.push(c); pointSet.push(d); pointSet.push(e); function getIndex(str) { for (let i = 0; i < pointSet.length; i++) { if (pointSet[i].value == str) return i; } return -1; } // 需要传入点的集合,边的集合,当前已经连接进入的集合 // 此方法,根据当前已经有的节点,来进行判断,获取距离最短的点 function getMinDistance(pointSet, distance, nowPointSet) { var formNode = null; //线段的起点 var minDisNode = null; //线段的终点 var minDis = max; // 根据目前已有的这些点为起点,判断连接其他的点的距离是多少 for (let i = 0; i < nowPointSet.length; i++) { var nowPointIndex = getIndex(nowPointSet[i].value); //获取当前节点的序号 for (let j = 0; j < distance[nowPointIndex].length; j++) { //遍历这个序号对于的行 var thisNode = pointSet[j]; //thisNode表示distance中的点 if (nowPointSet.indexOf(thisNode) < 0 //首先这个点不能是已经接入的点 && distance[nowPointIndex][j] < minDis) { //其次点之间的距离得是目前的最短距离 formNode = nowPointSet[i]; //起点 minDisNode = thisNode; //终点 minDis = distance[nowPointIndex][j]; //跟新最短距离 } } } formNode.neighbor.push(minDisNode); //起点的neighbor是终点 minDisNode.neighbor.push(formNode); return minDisNode; } function prim(pointSet, distance, start) { //普利姆算法(加点法) var nowPointSet = []; nowPointSet.push(start); // 获取最小代价的边 while (true) { var minDisNode = getMinDistance(pointSet, distance, nowPointSet) nowPointSet.push(minDisNode); if (nowPointSet.length == pointSet.length) { //结束条件 break; } } } prim(pointSet, distance, pointSet[2]) console.log(pointSet)
克鲁斯卡尔算法(加边法)
-
选择最短的边进行连接
-
要保证边与边连结的两端至少有一个点是新的点
-
或者这个边是将两个部落进行连结的
-
重复1-3直到将所有的点都连结到一起
克鲁斯卡尔算法代码实现(难)
var max = 1000000; var pointSet = []; var distance = [ [0, 4, 7, max, max], [4, 0, 8, 6, max], [7, 8, 0, 5, max], [max, 6, 5, 0, 7], [max, max, max, 7, 0] ] function Node(value) { this.value = value; this.neighbor = []; //图的一个节点可能有很多节点 } var a = new Node("A"); var b = new Node("B"); var c = new Node("C"); var d = new Node("D"); var e = new Node("E"); pointSet.push(a); pointSet.push(b); pointSet.push(c); pointSet.push(d); pointSet.push(e); function canLink(resultList, tempBegin, tempEnd) { //canLink方法判断是否可以连接的方法 :先判断两个点是否为新的点,如果不是,判断是否为两个部落中的点 var beginIn = null; //判断tempBegin是否在里面 var endIn = null; //判断tempEnd是否在里面 for (let i = 0; i < resultList.length; i++) { if (resultList[i].indexOf(tempBegin) > -1) { //tempBegin在resultList里面 beginIn = resultList[i]; } if (resultList[i].indexOf(tempEnd) > -1) { //tempEnd在resultList里面 endIn = resultList[i]; } } // 下面是不可以连接的情况 // begin和end在同一个部落 -----不可以连接 if (beginIn != null && endIn != null && beginIn == endIn) { return false } else { // 下面都是可以连接的情况(仅仅是判断能否连接的话,可以合并判断) // 两个点都是新的点(都不在任何部落)---产生新的部落,可以连接 // begin在A部落,end没有部落 ---- A部落扩张一个节点 // end在A部落,begin没有部落 ----- A部落扩张一个节点 // begin在A部落,end在B部落 ------将AB两个部落合并 return true } } function link(resultList, tempBegin, tempEnd) { var beginIn = null; //判断tempBegin是否在里面 var endIn = null; //判断tempEnd是否在里面 for (let i = 0; i < resultList.length; i++) { if (resultList[i].indexOf(tempBegin) > -1) { //tempBegin在resultList里面 beginIn = resultList[i]; } if (resultList[i].indexOf(tempEnd) > -1) { //tempEnd在resultList里面 endIn = resultList[i]; } } // 下面都是可以连接的情况(要判断怎么连接,不能合并,得一个个判断) if (beginIn == null && endIn == null) { // 两个点都是新的点(都不在任何部落)---产生新的部落,可以连接 var newArr = []; newArr.push(tempBegin); newArr.push(tempEnd); resultList.push(newArr); } else if (beginIn != null && endIn == null) { // begin在A部落,end没有部落 ---- A部落扩张一个节点 beginIn.push(tempEnd); } else if (beginIn == null && endIn != null) { // end在A部落,begin没有部落 ----- A部落扩张一个节点 end.push(tempBegin); } else if (beginIn != null && endIn != null && beginIn != endIn) { // begin在A部落,end在B部落 ------将AB两个部落合并 var allIn = beginIn.concat(endIn); var needRemove1 = resultList.indexOf(endIn); resultList.splice(needRemove1, 1); var needRemove2 = resultList.indexOf(beginIn); resultList.splice(needRemove2, 1); resultList.push(allIn); } // 上一个函数判断可以连接 tempBegin.neighbor.push(tempEnd); tempEnd.neighbor.push(tempBegin); } function kruskal(pointSet, distance) { var resultList = []; //这里是二位数组,此数组代表的是有多少个“部落” while (true) { //知道次数的用for,不知道次数的用while循环 var minDis = max; //定义最短距离,默认max var begin = null; //定义起点 var end = null; //定义终点 for (let i = 0; i < distance.length; i++) { for (let j = 0; j < distance[i].length; j++) { var tempBegin = pointSet[i]; var tempEnd = pointSet[j]; if (i != j //去掉自己到自己的距离,因为都为0 && distance[i][j] < minDis && canLink(resultList, tempBegin, tempEnd) //canLink方法判断是否可以连接 ) { minDis = distance[i][j]; begin = tempBegin; end = tempEnd; } } } link(resultList, begin, end); //连接两个点,添加进去一条线 if (resultList.length == 1 && resultList[0].length == pointSet.length) { //只存在一个部落,并且这个部落中的点把所有的点都连接起来了 break; } } } kruskal(pointSet, distance) console.log(pointSet)
二叉搜索树(二叉排序树)
有排序的效果,左子树的节点都比当前节点小,右子树的节点都比当前节点大
构建二叉搜索树
var arr = []; for (let i = 0; i < 10; i++) { arr[i] = Math.floor(Math.random() * 10) } function Node(value) { this.value = value; this.left = null; this.right = null; } function addNode(root, num) { if (root == null) return; if (root.value == num) return; if (root.value < num) { //目标值比当前节点大 if (root.right == null) { root.right = new Node(num); } else { addNode(root.right, num); } } else { //目标值比当前节点小 if (root.left == null) { root.left = new Node(num); } else { addNode(root.left, num); } } } function buildSearchTree(arr) { if (arr == null) return null; var root = new Node(arr[0]); for (let i = 1; i < arr.length; i++) { addNode(root, arr[i]) } return root; } var root = buildSearchTree(arr); //得到二叉搜索树 console.log(root);
二叉搜索树的使用
// 二叉搜索树查找某一个数 function searchByTree(root, target) { if (root == null) return false; if(root.value == target ) return true; if(root.value > target){ return searchByTree(root.left,target); }else{ return searchByTree(root.right,target); } } console.log(searchByTree(root,1000));
查找一个数,数组于平衡二叉树的性能比较(即比较次数)
// 比较普通数组和二叉搜索树,搜索一个数是否存在的性能 //比较次数差很多 //数的搜索性能是由树的深度来决定的,想要提升性能,可以使用平衡二叉搜索树(树的深度尽量浅) var arr = []; for (let i = 0; i < 10000; i++) { arr[i] = Math.floor(Math.random() * 10000) } var num1 = 0; function search(arr, target) { for (let i = 0; i < arr.length; i++) { num1 += 1; if (arr[i] == target) return true; } return false; } console.log(search(arr, 1000)); console.log(num1); function Node(value) { this.value = value; this.left = null; this.right = null; } function addNode(root, num) { if (root == null) return; if (root.value == num) return; if (root.value < num) { //目标值比当前节点大 if (root.right == null) { root.right = new Node(num); } else { addNode(root.right, num); } } else { //目标值比当前节点小 if (root.left == null) { root.left = new Node(num); } else { addNode(root.left, num); } } } var num2 = 0; function buildSearchTree(arr) { if (arr == null) return null; var root = new Node(arr[0]); for (let i = 1; i < arr.length; i++) { addNode(root, arr[i]) } return root; } var root = buildSearchTree(arr); //得到二叉搜索树 // 二叉搜索树查找某一个数 function searchByTree(root, target) { num2++ if (root == null) return false; if(root.value == target ) return true; if(root.value > target){ return searchByTree(root.left,target); }else{ return searchByTree(root.right,target); } } console.log(searchByTree(root,1000)); console.log(num2);
平衡二叉树
-
根节点的左子树与右子树的高度差不能超过1
-
这棵二叉树的每个子树都符合第一条
代码实现判断平衡二叉树
root是构建好的平衡二叉树
function Node(value) { this.value = value; this.left = null; this.right = null; } var a= new Node("a"); var b= new Node("b"); var c= new Node("c"); var d= new Node("d"); var e= new Node("e"); var f= new Node("f"); var g= new Node("g"); var h= new Node("h"); var j= new Node("j"); function getDeep(root) { if(root == null) return 0; var leftDeep = getDeep(root.left); var rightDeep = getDeep(root.right); return Math.max(leftDeep,rightDeep) + 1; } function isBalance(root) { if(root == null) return true; var leftDeep = getDeep(root.left); var rightDeep = getDeep(root.right); if(Math.abs(leftDeep - rightDeep) > 1){ return false; }else{ return isBalance(root.left) && isBalance(root.right); } } console.log(isBalance(a))
二叉树的单旋
二叉树双旋
二叉树双旋的代码实现
左左双旋和右右双旋
234树的由来
红黑树
普通树
普通树(一个节点有多个子节点,所以应该是childs)
// 节点创建 function Node(value) { this.value = value; this.childs = []; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); var f = new Node("f"); a.chidls.push(c); a.chidls.push(b); a.chidls.push(f); b.chidls.push(d); b.chidls.push(e);
树的深度优先搜索
function deepSearch(root,target) { if(root==null) return false; if(root.value == target){ return true; } var result = false; for(var i =0;i<root.childs.length;i++){ result |= deepSearch(root.childs[i],target); } return result; } deepSearch(a,"c");
树的广度优先搜索
function bfs(roots,target) { //roots接受一个数组 if(rootss =null || roots.length == 0 ) return false; var childs = []; for(var i = 0;i<roots.length ;i++){ if(roots[i].value ==target){ return true; }else{ chidls = childs.concat(roots[i].childs); } } return bfs(chidls,target); }
构建一个图
// 节点创建 function Node(value) { this.value = value; this.neighbor = []; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); a.neighbor.push(b); a.neighbor.push(c); b.neighbor.push(a); b.neighbor.push(c); b.neighbor.push(d); c.neighbor.push(a); c.neighbor.push(b); c.neighbor.push(d); d.neighbor.push(b); d.neighbor.push(c); d.neighbor.push(e); e.neighbor.push(d);
图的深度优先搜索
// 节点创建 function Node(value) { this.value = value; this.neighbor = []; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); a.neighbor.push(b); a.neighbor.push(c); b.neighbor.push(a); b.neighbor.push(c); b.neighbor.push(d); c.neighbor.push(a); c.neighbor.push(b); c.neighbor.push(d); d.neighbor.push(b); d.neighbor.push(c); d.neighbor.push(e); e.neighbor.push(d); function deepSearch(node, target, path) { if (node == null) { return false; } if (path.indexOf(node) > -1) { return false; } if (node.value == target) { return true; } path.push(node); var result = false; for (let i = 0; i < node.neighbor.length; i++) { result |= deepSearch(node.neighbor[i], target, path); } return result ? true : false; } console.log(deepSearch(b, "c", []));
图的广度优先搜索
// 节点创建 function Node(value) { this.value = value; this.neighbor = []; } var a = new Node("a"); var b = new Node("b"); var c = new Node("c"); var d = new Node("d"); var e = new Node("e"); a.neighbor.push(b); a.neighbor.push(c); b.neighbor.push(a); b.neighbor.push(c); b.neighbor.push(d); c.neighbor.push(a); c.neighbor.push(b); c.neighbor.push(d); d.neighbor.push(b); d.neighbor.push(c); d.neighbor.push(e); e.neighbor.push(d); function bfs(nodes, target, path) { if(nodes == null || nodes.length ==0) return false; var nextNodes = []; for (let i = 0; i < nodes.length; i++) { if (path.indexOf(nodes[i]) > -1) { continue; } path.push(nodes[i]); if (nodes[i].value == target) { return true; }else{ nextNodes = nextNodes.concat(nodes[i].neighbor); } return bfs(nextNodes,target,path); } } console.log(bfs([c], "b", []));
动态规划
动态规划,笔试遇到动态规划是后面的大题,校招必考题
通常给出如下题:-----斐波那契数列
动态规划之斐波那契数列
// 0,1,1,2,3,5,8,13,21.... // 给出第n位,问第n位值为几? // 循环的方式 function fibo(n) { if (n <= 0) return -1; if (n == 1) return 0; if (n == 2) return 1; var a = 0; var b = 1; var c; for (let i = 3; i <= n; i++) { c = a + b; a = b; b = c; } return c; } // 递归 f(n) = f(n-1) + f(n-2) function fibo2(n) { if (n <= 0) return -1; if (n == 1) return 0; if (n == 0) return 1; return fibo2(n-1) +fibo2(n-2); } console.log(fibo(4));
动态规划之青蛙跳台阶问题(笔试中最常见的题)
//一个青蛙,一次只能跳一级台阶,或者跳两级台阶 // 问:这个青蛙跳上n级台阶有多少种跳法? // 如果这只青蛙,跳上了第n级台阶,那么最后一次跳跃之前,他一定在n-1级台阶或者n-2级台阶上。 // 那么跳上n级台阶有多少情况就变成了两个子问题 // 跳上n-1级台阶的跳法,加上,跳上n-2级台阶的跳法。 // 按照此逻辑递推,跳上n-1级台阶可以拆解为两个子问题 // 即:跳上n-2级台阶的跳法,加上,跳上n-3级台阶的跳法 // f(n)= f(n-1) + f(n-2) function jump(n) { if (n <= 0) return -1; if (n == 1) return 1; if (n == 2) return 2; return jump(n - 1) + jump(n - 2); }
动态规划之变态青蛙跳台阶
// 变态青蛙跳台阶问题 // 这只青蛙,一次可以跳1级台阶、2级台阶、或者n级台阶。 // 问:这只青蛙,跳上n级台阶有多少种方法 // f(n) = f(n-1) + f(n-2)+f(n-3) +.... +f(2) +f(1)+f(0) function jump(n) { if (n <= 0) return -1; if (n == 1) return 1; if (n == 2) return 2; var result = 0; for (let i = 1; i < n; i++) { result += jump(n - i); //最少中一级台阶开始跳,避免死循环 } return result + 1; // +1表示从0级台阶直接跳上去的情况。 } // n为3时有哪些情况 // 1,1,1 // 1.2 // 2,1 // 2 // n为4时有哪些情况 // 1,1,1,1 // 1,1,2 // 1,2,1 // 2,1,1 // 1,3 // 3,1 // 2,2 // 4 console.log(jump(4))
如果算法导论都没有问题,看看《算法导论》