前端演算法 - 資料結構

JsRicardo發表於2019-12-04

資料結構分為

  1. 線性資料結構
  2. 二維資料結構

線性資料結構

線性資料結構強調的儲存和順序。

一維陣列

陣列特性

  • 非常重要的一點,也是被前端程式設計師忽視的一點,陣列是定長的。也就是陣列的長度是固定的,因為在作業系統上來說,陣列的儲存必須是一段連續的空間。這也是為什麼資料量一大,通常不會用陣列來儲存。
  • 為什麼JS的陣列可以隨意改變長度?那是JS引擎在底層實現的,當改變陣列長度時,會對陣列進行擴容操作,而擴容又是比較消耗效能的(因為擴容時,會新在記憶體中宣告一個空間,再把之前的陣列拷貝過去)。也就是說在宣告陣列的時候,可以大概給定一個長度,避免頻繁的擴容操作。
  • 陣列的變數,指向了陣列第一個元素的位置。var a = {1,2,3,4,5}; // a 指向 1 的位置

優點:查詢效能好,指定查詢某個位置;在作業系統中,通過偏移查詢資料效能好

缺點:

  1. 因為陣列的空間必須是連續的,所以陣列比較大的情況,當系統的空間碎片較多的時候,容易存不下;
  2. 因為陣列的長度是固定的,所以陣列的內容難以被新增和刪除;

陣列的操作

  • 排序

    排序的本質就是比較和交換,而不是比較大小。


// 比較 (其實Array.prototype.sort傳遞的函式,就是這個比較函式)
function compare(a, b) {
    if (a > b) return true
    else return false
}
// 交換
function exchange(arr, idxa, idxb) {
    var temp = arr[idxa]
    arr[idxa] = arr[idxb]
    arr[idxb] = temp
}
複製程式碼

氣泡排序

  • 迴圈,比較,交換
  • 每一次迴圈,將最大的數推到最後面
  • 迴圈這步操作
// 迴圈
function sort(arr) {
    if (arr == null || arr.length <= 1) return arr
    for (var i = 0; i < arr.length; i++) {
        // 不用比較已經比較了的位置,比較j, j+1,所以取到j—1
        for (var j = 0; j < arr.length - 1 - i; j++) {
            if (compare(arr[j], arr[j + 1])) {
                exchange(arr, j, j + 1)
            }
        }
    }
}
複製程式碼

選擇排序

  • 第一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在序列的起始位置
  • 然後再從剩餘的未排序元素中尋找到最小(大)元素,然後放到已排序的序列的末尾
  • 迴圈直到全部待排序的資料元素的個數為零
// 選擇排序
function choose(arr) {
    for (var i = 0; i < arr.length; i++) {
        var maxIndex = 0;
        for(var j = 0; j < arr.length - i; j++) {
            if (compare(arr[j], arr[j + 1])){
                maxIndex = j
            }
        }
        exchange(arr[maxIndex], arr[arr.length - i])
    }
}
複製程式碼

快速排序

  • 簡單快排
function quickSort (arr) {
    if(arr == null || arr.length <= 1) return arr;
    // 1. 選出一個標準位置
    var location = arr[0];
    // 2. 比較,比標準位置大的放一邊,小的放另一邊
    var left = [], right = [];
    // 因為 0 已經做標準位置了,所以從 1 開始迴圈
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] < location) left.push(arr[i]);
        else right.push(arr[i]);
    };
    quickSort(left);
    quickSort(right);
    left.push(location);
    return left.concat(right);
}
複製程式碼

簡單快速排序會不斷的建立新的陣列,犧牲了大量的空間,但是便於理解。

  • 標準快排

不建立新的陣列,用陣列的index作為指標,對原陣列進行操作

不會做gif圖 很尷尬...

// 2. 標準快速排序
function realyQuickSort (arr, begin = 0, end = arr.length) {
    if(arr == null || arr.length <= 1) return arr;
    if (begin = end -1) return; // 兩個指標相差為1就不用比較了 挨著了
    // 建立左 右指標
    var left = begin, right = end;
    do {
        // 移動指標 當遇到左指標對應的數大於標準位置時,暫時停止一次
        // 當遇到右指標小於標準位置時,暫時停止一次
        // 交換兩個指標對應的陣列值
        do left++; 
            while(left < right && arr[left] < arr[begin]);
        do right--; 
            while(right > left && arr[right] > arr[begin]);
        if (left < right) exchange(arr, left, right);
    } while (left < right);
    var exchangePoint = left == right ? right - 1 : right;
    exchange(arr, begin, exchangePoint)
    // 交換完一次後得到標準位置在中間的結果陣列,遞迴操作
    realyQuickSort(arr, begin, exchangePoint)
    realyQuickSort(arr, exchangePoint + 1, end)
}
複製程式碼

連結串列

單連結串列

想要傳遞一個連結串列,必須傳遞連結串列的根節點,而每一個節點都認為自己的根節點

一般討論連結串列,都指單連結串列,雙連結串列實現的功能,都可以用單連結串列實現

  1. 上一個物件 存著下一個物件的引用
var b = {
    value: 1,
    next: null,
}
var a = {
    value: 2,
    next: b
}
console.log(a.next === b) // true
複製程式碼
  1. 連結串列的特點

    1). 空間上不是連續的

    2). 每存放一個值,都需要多開銷一個引用空間

  • 優點:
  1. 只要記憶體足夠大,就能存的下,不用擔心空間碎片的問題;
  2. 連結串列的新增和刪除非常的容易;
  • 缺點:
  1. 查詢速度慢(查詢某個位置)
  2. 連結串列每一個節點都需要建立一個指向next的引用,浪費了一定記憶體空間,當儲存的資料越多時,這部分開銷的記憶體影響越小。
function Node (value) {
    this.value = value;
    this.next = null;
}
var a = new Node(1);
var b = new Node(2);
var c = new Node(3);
a.next = b;
b.next = c;
console.log(a.next.value); // 2
複製程式碼

雙向連結串列

  • 沒有根節點的概念 雙向連結串列一般不使用
  1. 優點: 可以雙向查詢,方便
  2. 缺點: 多開銷一個引用空間,構建麻煩
function Node(val) {
    this.value = value
    this.next = null
    this.prev = null
}

var node1 = new Node(1)
var node2 = new Node(2)
var node3 = new Node(3)

node1.next = node2
node2.prev = node1
node2.next = node3
node3.prev = node2
複製程式碼

連結串列的操作

  1. 連結串列的遍歷
// 迴圈遍歷
function bian(root) {
    var temp = root;
    while (true) {
        if (temp != null) {
            console.log(temp.value);
        } else {
            break;
        };
        temp = temp.next;
    }
}
// 遞迴遍歷
function digui (root) {
    if (root == null) return;
    console.log(root.value);
    digui(root.next);
}
複製程式碼

一般在遍歷時,都使用遞迴遍歷。

  1. 連結串列的逆置

連結串列的逆置就是將連結串列倒轉過來

/* 1. 找到連結串列的倒數第二個節點
* 2.倒數第一個節點就是倒數第二個節點的next
* 3. 將倒數第一個節點的next指向倒數第二個節點
* 4. 現在倒數第一個節點就是新的根節點
*/
function reverseLink(root) {
    if (root.next.next == null){
        root.next.next = root;
        return root.next;
    } else {
        var res = reverseLink(root.next)
        root.next.next = root
        root.next = null
        return res
    }
}
const reverse = reverseLink(a)
複製程式碼

棧是一種後進先出的資料結構,也就是說最新新增的項最早被移出;

它是一種運算受限的線性表,只能在表頭/棧頂進行插入和刪除操作。

棧有棧底和棧頂。入棧是把新元素放入到棧頂的上面,成為新的棧頂;出棧之後相鄰的成為新棧頂;也就是說棧裡面的元素的插入和刪除操作,只在棧頂進行。

小擴充:JS函式裡面有作用域的概念,有GO AO 的概念,而上面有一個只供系統使用的[[scop]]屬性,裡面存的就是這些GO AO, 底層實現原理估計就是棧資料結構,看來什麼時候得深入瞭解一下。

得益於JS的陣列特性,JS實現棧和佇列資料結構都非常的方便

var arr = []

// 棧 先入後出
class MyStack {
    arr = []
    // 入棧
    push(val){
        this.arr.push(val)
        return this.arr
    }
    // 出棧
    pop(){
        return this.arr.pop()
    }
}

const stack = new MyStack()

stack.push(1)
stack.push(12)
stack.push(14)
stack.push(15)

console.log(stack.pop()) //15
console.log(stack.pop()) //14
console.log(stack.push(19)) // [1, 12, 19]
console.log(stack.pop()) // 19
複製程式碼

佇列

佇列是一種先進先出的資料結構。

佇列在列表的末端增加項,在首端移除項。

它允許在表的首端(佇列頭)進行刪除操作,在表的末端(佇列尾)進行插入操作。

眾所周知:佇列是實現多工的重要機制!

// 佇列 先入先出
class Queen {
    arr = []
    // 入隊
    push (val) {
        this.arr.push(val)
        return this.arr
    }
    // 出隊
    pop () {
        return this.arr.shift()
    }
}

const queen = new Queen()

queen.push(1)
queen.push(12)
queen.push(14)
queen.push(15)

console.log(queen.pop()) // 1
console.log(queen.pop()) // 12
console.log(queen.push(19)) // [ 14, 15, 19 ]
console.log(queen.pop()) // 14
複製程式碼

二維資料結構

二維陣列

這個應該沒什麼好說的了,就是陣列

var arr = [ [12, 34], [21, 2123], [111, 876] ]

for(var i = 0; i < arr.length; i++){
    for(var j = 0; j < arr[i].length, j++){
        console.log(arr[i][j])
    }
}
複製程式碼

螺旋矩陣問題

leetCode第54題:螺旋矩陣

螺旋矩陣就是給定一個二維陣列,需要像貪吃蛇一樣繞圈往內層旋轉,將這個二維陣列變成一維陣列。

處理這種問題 主要就是要處理邊界問題,每一次讀取了一層之後,下一次就要往內層移動一層。

const arr: number[][] = [
    [1, 2, 3, 4],
    [12, 13, 14, 5],
    [11, 16, 15, 6],
    [10, 9, 8, 7],
];
// 這個陣列按照螺旋矩陣旋轉之後,返回的陣列就應該是[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
// 從左往右,再往下,在往上,再往右迴圈。

function luoxuan(arr: number[][]): number[] {
    // tslint:disable-next-line: curly
    if (arr.length === 0 || !arr) return [];
    const result: number[] = [];
    let left = 0,
            right = arr[0].length - 1,
            top = 0,
            bottom = arr.length - 1,
            direction = "right";
    while (left <= right && top <= bottom) { // 相等時也需要再迴圈一次,否則會漏掉一層
            if (direction === "right") { // 從左往右
                for (let i = left; i <= right; i++) {
                    result.push(arr[top][i]); // 從上方左往右的過程中,高度是不變的
                }
                top++; // 迴圈完之後,相當於高度削減了一層,下一次不走這一層了
                direction = "bottom";
            }
            if (direction === "bottom") {
                for (let i = top; i <= bottom; i++) {
                    result.push(arr[i][right]);
                }
                right--;
                direction = "left";
            }
            if (direction === "left") {
                for (let i = right; i >= left; i--) {
                    result.push(arr[bottom][i]);
                }
                bottom--;
                direction = "top";
            }
            if (direction === "top") {
                for (let i = bottom; i >= top; i--) {
                    result.push(arr[i][left]);
                }
                left++;
                direction = "right";
            }
        }
    return result;
}
複製程式碼

二維拓撲結構

二維拓撲結構,專業術語上也稱

  • 二叉樹,多叉樹其實都是拓撲結構
// 拓撲結構
function Node (value) {
    this.value = value
    this.neighbor = []
}

var node1 = new Node(1)
var node2 = new Node(2)
var node3 = new Node(3)
var node4 = new Node(4)

node1.neighbor.push(node2, node3)
node2.neighbor.push(node1, node4)
node3.neighbor.push(node1, node4)
node4.neighbor.push(nod2, node3)
複製程式碼

樹形結構

  • 樹是圖的一種
  • 樹有一個根節點
  • 樹沒有環形結構(迴路)
  • 度: 樹的最多叉的節點有多少叉,就有多少度
  • 深度: 樹最深有多少層,就是多少深度

在生活中樹形結構的資料應用,多的不能再多了吧:dom樹,目錄樹,關係樹...

最後

資料結構,大概先記這麼多,前端中這些資料結構的演算法、二叉樹、紅黑樹、斐波那契數列、動態規劃啥的,

且聽下回分解

相關文章