前端必会算法(原理及代码实现)

前端必会算法

推荐书籍:算法导论

一维数据结构:线性数据结构(数组,链表),线性的数据结构强调存储与顺序

线性数据结构之数组

数组特性:

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--;
  }
}

 

  1. 希尔排序(性能最好的排序)

    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.左子树、右子树

 

 

二叉树的遍历(几乎校招每年的必考题)

考点:

  1. 给出二叉树,写成前序中序后序遍历

  2. 写成前序中序后序遍历的代码

  3. 给出前序中序还原二叉树,要求写出后序遍历(中序一定要有)

  4. 给出后序中序还原二叉树,要求写出前序遍历(中序一定要有)

  5. 代码实现前序中序还原二叉树

  6. 代码实现中序后序还原二叉树

 

传递二叉树要传递根节点。

前序遍历(先根次序遍历):先打印当前的(中间的),再打印左边的,最后打印右边的

中序遍历(中根次序遍历):先打印左边的,再打印当前的,最后打印右边的(中序遍历其实就是从左到右进行投影)

后序遍历(后根次序遍历):先打印左边的,再打印右边的,最后打印当前的

打印顺序:前序遍历根节点在最左边,中序遍历根节点在中间,后序遍历根节点在最右边

 

代码实现前中后序

节点创建
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算法

两棵树的比较:新增,修改,删除什么?


 

最小生成树(一个图生成最小生成树)

06c64334d6b42e8d743bf887805ff3f

表示一个图,可以使用点集合和边集合

点集合:[a,b,c,d,e,f]

 ABCDE
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

比如:需要将所有的村庄联通,但需要花费最少的钱?

普利姆算法(加点法)

  1. 任选一个点作为起点

  2. 找到以当前选中点为起点路径最短的边

  3. 如果这个边的另一端没有被联通进来,那么就连结

  4. 如果这个边的另一端也早就被连进来了,则看倒数第二短的边

  5. 重复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. 选择最短的边进行连接

  2. 要保证边与边连结的两端至少有一个点是新的点

  3. 或者这个边是将两个部落进行连结的

  4. 重复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. 根节点的左子树与右子树的高度差不能超过1

  2. 这棵二叉树的每个子树都符合第一条

代码实现判断平衡二叉树

1618673475902

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);

图的深度优先搜索

b86391319ea6280f2a530e101666339

// 节点创建
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))

 

如果算法导论都没有问题,看看《算法导论》

 

 

posted @ 2021-04-18 11:12  木杉呀  阅读(712)  评论(0编辑  收藏  举报