推薦書籍:算法導論
一維數據結構:線性數據結構(數組,鏈表),線性的數據結構強調存儲與順序
線性數據結構之數組
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))
如果算法導論都沒有問題,看看《算法導論》