前端必會算法(原理及代碼實現)


前端必會算法

推薦書籍:算法導論

一維數據結構:線性數據結構(數組,鏈表),線性的數據結構強調存儲與順序

線性數據結構之數組

數組特性:

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]

  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

比如:需要將所有的村庄聯通,但需要花費最少的錢?

普利姆算法(加點法)

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

 

如果算法導論都沒有問題,看看《算法導論》

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM