JavaScript 資料結構與基礎演算法

SRIGT發表於2024-07-30

資料結構全解參考:資料結構 | 部落格園-SRIGT

相關程式碼倉庫檢視:data-struct-js | Github-SR1GT

0x00 前置知識

(1)類

  1. 使用關鍵字 class 宣告一個類

    class Person {
      
    }
    
  2. JavaScript 的類中透過 constructor 使用構建函式

    class Person {
      constructor(name) {
        this.name = name;
      }
    }
    
  3. 定義 Getter 與 Setter

    class Person {
      constructor(name) {
        this.name = name;
      }
      getName() {
        return this.name;
      }
      setName(name) {
        this.name = name;
      }
    }
    
  4. 使用關鍵字 new 建立物件,並呼叫物件方法

    class Person {
      constructor(name) {
        this.name = name;
      }
      getName() {
        return this.name;
      }
      setName(name) {
        this.name = name;
      }
    }
    
    var person = new Person("SRIGT");
    console.log(person.getName());		// "SRIGT"
    person.setName("SR1GT");
    console.log(person.getName());		// "SR1GT"
    

(2)指標

  • 以下程式碼僅實現值的傳遞,並未涉及指標

    var num1 = 1;
    var num2 = num1;
    console.log(num1, num2);	// 1 1
    num1 = 2;
    console.log(num1, num2);	// 2 1
    
  • 當宣告的變數是物件時,實現指標的傳遞

    var num1 = { value: 1 };
    var num2 = num1;
    console.log(num1.value, num2.value);	// 1 1
    num1.value = 2;
    console.log(num1.value, num2.value);	// 2 2
    
    • var num2 = num1; 是將變數 num2 指向 num1,同時 num1 又指向物件 { value: 1 }
  • 當某個物件不被任何變數指向時,該物件不可訪問,但依舊佔用記憶體空間,此時 JavaScript 提供的垃圾回收機制很好的解決了這個問題

(3)遞迴

  • 遞迴是指在函式中呼叫自身

    function func() {
      if (condition === true) return;
      func();
    }
    
    • 當沒有合適條件停止遞迴,會造成堆疊溢位
  • 舉例:階乘

    function factorial(n) {
      if (n === 1) return 1;
      return n * factorial(n - 1);
    }
    
    console.log(factorial(4));		// 24
    

0x01 連結串列

(1)單連結串列

  1. 宣告類並實現建構函式

    class Node {
      constructor(value) {
        this.value = value;
        this.next = null;
      }
    }
    
    class LinkedList {
      constructor(value) {
        this.head = new Node(value);
        this.tail = this.head;
        this.length = 1;
      }
    }
    
    var myLinkedList = new LinkedList(1);
    console.log(myLinkedList);
    
  2. LinkedList 類中,新增 push 方法,用於在尾部新增節點

    push(value) {
      const newNode = new Node(value);
      if (!this.head) {
        this.head = newNode;
        this.tail = this.head;
      } else {
        this.tail.next = newNode;
        this.tail = newNode;
      }
      this.length++;
      return this;
    }
    
  3. 新增 pop 方法,用於刪除尾部最後一個節點

    pop() {
      if (!this.head) return undefined;
    
      let prev = this.head;
      let temp = this.head;
      while (temp.next) {
        prev = temp;
        temp = temp.next;
      }
      this.tail = prev;
      this.tail.next = null;
      this.length--;
    
      if (this.length === 0) this.head = this.tail = null;
      return temp;
    }
    
  4. 新增 unshiftshift 方法,分別用於在頭部新增和刪除第一個節點

    unshift(value) {
      const newNode = new Node(value);
      if (!this.head) {
        this.head = newNode;
        this.tail = this.head;
      } else {
        newNode.next = this.head;
        this.head = newNode;
      }
      this.length++;
      return this;
    }
    
    shift() {
      if (!this.head) return undefined;
    
      let temp = this.head;
      this.head = this.head.next;
      temp.next = null;
      this.length--;
    
      if (this.length === 0) this.tail = null;
      return temp;
    }
    
  5. 新增 get 方法,用於獲取特定索引位置的值

    get(index) {
      if (index === 0) return this.head;
      if (index < this.length * -1 || index >= this.length)
        throw new Error("Index out of bounds");
      if (index < 0) index = this.length + index;
      let temp = this.head;
      for (let i = 0; i < index; i++) temp = temp.next;
      return temp;
    }
    
  6. 新增 set 方法,用於修改指定位置的值

    set(index, value) {
      let temp = this.get(index);
      if (!temp) return false;
      temp.value = value;
      return true;
    }
    
  7. 新增 insert 方法,用於在指定位置新增新節點

    insert(index, value) {
      if (index === 0) return this.unshift(value);
      if (index === this.length) return this.push(value);
      if (index < 0 || index > this.length)
        throw new Error("Index out of bounds");
    
      let newNode = new Node(value);
      let prev = this.get(index - 1);
      newNode.next = prev.next;
      prev.next = newNode;
      this.length++;
      return this;
    }
    
  8. 新增 remove 方法,用於刪除指定位置的節點

    remove(index) {
      if (index === 0) return this.shift();
      if (index === this.length - 1) return this.pop();
      if (index < 0 || index >= this.length)
        throw new Error("Index out of bounds");
    
      let prev = this.get(index - 1);
      let temp = prev.next;
      prev.next = temp.next;
      temp.next = null;
      this.length--;
      return temp;
    }
    
  9. 新增 reverse 方法,用於翻轉連結串列

    reverse() {
      let temp = this.head;
      let next = temp.next,
        prev = null;
    
      [this.head, this.tail] = [this.tail, this.head];
      for (let i = 0; i < this.length; i++) {
        next = temp.next;
        temp.next = prev;
        prev = temp;
        temp = next;
      }
      return this;
    }
    
  10. 新增 forEach 方法,用於遍歷連結串列

    forEach(callback) {
      if (!this.head) return;
      let temp = this.head;
      while (temp) {
        callback(temp);
        temp = temp.next;
      }
    }
    

(2)雙向連結串列

  1. 宣告類並實現建構函式

    class Node {
      constructor(value) {
        this.value = value;
        this.prev = null;
        this.next = null;
      }
    }
    
    class DoublyLinkedList {
      constructor(value) {
        this.head = new Node(value);
        this.tail = this.head;
        this.length = 1;
      }
    }
    
    var myDoublyLinkedList = new DoublyLinkedList(1);
    console.log(myDoublyLinkedList);
    
  2. pushpop 方法

    push(value) {
      const newNode = new Node(value);
      if (!this.head) {
        this.head = newNode;
        this.tail = this.head;
      } else {
        newNode.prev = this.tail;
        this.tail.next = newNode;
        this.tail = newNode;
      }
      this.length++;
      return this;
    }
    
    pop() {
      if (!this.head) return undefined;
    
      let temp = this.tail;
      this.tail = this.tail.prev;
      this.tail.next = null;
      temp.prev = null;
      this.length--;
    
      if (this.length === 0) this.head = this.tail = null;
      return temp;
    }
    
  3. unshiftshift 方法

    unshift(value) {
      const newNode = new Node(value);
      if (!this.head) {
        this.head = newNode;
        this.tail = this.head;
      } else {
        this.head.prev = newNode;
        newNode.next = this.head;
        this.head = newNode;
      }
      this.length++;
      return this;
    }
    
    shift() {
      if (!this.head) return undefined;
    
      let temp = this.head;
      this.head = this.head.next;
      this.head.prev = null;
      temp.next = null;
      this.length--;
    
      if (this.length === 0) this.tail = null;
      return temp;
    }
    
  4. getset 方法

    get(index) {
      if (index === 0) return this.head;
      if (index === this.length - 1 || index === -1) return this.tail;
      if (index < this.length * -1 || index >= this.length)
        throw new Error("Index out of bounds");
    
      // 最佳化方向: 折半查詢
    
      if (index < 0) {
        let temp = this.tail;
        for (let i = this.length - 1; i > this.length + index; i--)
          temp = temp.prev;
        return temp;
      } else {
        let temp = this.head;
        for (let i = 0; i < index; i++) temp = temp.next;
        return temp;
      }
    }
    
    set(index, value) {
      let temp = this.get(index);
      temp.value = value;
      return temp;
    }
    
  5. insertremove 方法

    insert(index, value) {
      if (index === 0) return this.unshift(value);
      if (index === this.length) return this.push(value);
      if (index < 0 || index > this.length)
        throw new Error("Index out of bounds");
    
      let newNode = new Node(value);
      let prev = this.get(index - 1);
    
      newNode.next = prev.next;
      prev.next.prev = newNode;
    
      prev.next = newNode;
      newNode.prev = prev;
    
      this.length++;
      return this;
    }
    
    remove(index) {
      if (index === 0) return this.shift();
      if (index === this.length - 1) return this.pop();
      if (index < 0 || index >= this.length)
        throw new Error("Index out of bounds");
    
      let temp = this.get(index);
    
      temp.prev.next = temp.next;
      temp.next.prev = temp.prev;
      temp.prev = null;
      temp.next = null;
      this.length--;
      return temp;
    }
    

(3)棧

  1. 宣告類並實現建構函式

    class Node {
      constructor(value) {
        this.value = value;
        this.next = null;
      }
    }
    
    class Stack {
      constructor(value) {
        this.top = new Node(value);
        this.length = 1;
      }
    }
    
    var myStack = new Stack(1);
    console.log(myStack);
    
  2. 壓棧方法 push

    push(value) {
      const newNode = new Node(value);
      if (!this.top) this.top = newNode;
      else {
        newNode.next = this.top;
        this.top = newNode;
      }
      this.length++;
      return this;
    }
    
  3. 出棧方法 pop

    pop() {
      if (!this.top) return undefined;
      let temp = this.top;
      this.top = this.top.next;
      temp.next = null;
      this.length--;
      if (this.length === 0) this.top = null;
      return temp;
    }
    

(4)佇列

  1. 宣告類並實現建構函式

    class Node {
      constructor(value) {
        this.value = value;
        this.next = null;
      }
    }
    
    class Queue {
      constructor(value) {
        this.first = new Node(value);
        this.last = this.first;
        this.length = 1;
      }
    }
    
    var myQueue = new Queue(1);
    console.log(myQueue);
    
  2. 入隊方法 enQueue

    enQueue(value) {
      const newNode = new Node(value);
      if (!this.first) {
        this.first = newNode;
        this.last = this.first;
      } else {
        this.last.next = newNode;
        this.last = newNode;
      }
      this.length++;
      return this;
    }
    
  3. 出隊方法 deQueue

    deQueue() {
      if (!this.first) return undefined;
      let temp = this.first;
      this.first = this.first.next;
      temp.next = null;
      this.length--;
      if (this.length === 0) this.first = this.last = null;
      return temp;
    }
    

0x02 樹與表

(1)二叉搜尋樹

  1. 宣告類並實現建構函式

    class Node {
      constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
      }
    }
    
    class BinarySearchTree {
      constructor() {
        this.root = null;
      }
    }
    
    var myBST = new BinarySearchTree();
    console.log(myBST);
    
  2. 新增 insert 方法,用於新增節點

    insert(value) {
      const newNode = new Node(value);
      if (!this.root) {
        this.root = newNode;
        return this;
      } 
    
      let temp = this.root;
      while (true) {
        if (temp.value === value) return undefined;
        else if (temp.value > value) {
          if (!temp.left) {
            temp.left = newNode;
            return this;
          }
          temp = temp.left;
        } else {
          if (!temp.right) {
            temp.right = newNode;
            return this;
          }
          temp = temp.right;
        }
      }
    }
    
  3. 新增 contains 方法,用於判斷節點是否存在

    contains(value) {
      if (!this.root) return false;
    
      let temp = this.root;
      while (true) {
        if (temp.value === value) return true;
        else if (temp.value > value) {
          if (!temp.left) return false;
          temp = temp.left;
        } else {
          if (!temp.right) return false;
          temp = temp.right;
        }
      }
    }
    
  4. 新增 minValueNode 方法,獲取最小節點值

    minValueNode() {
      let temp = this.root;
      while (temp.left) temp = temp.left;
      return temp;
    }
    

(2)雜湊表

  1. 宣告類並實現建構函式

    class HashTable {
      constructor(size = 10) {
        this.dataMap = new Array(size);
      }
    
      _hash(key) {
        let hash = 0;
        for (let i in key)
          hash = (hash + key.charCodeAt(i) * 23) % this.dataMap.length;
        return hash;
      }
    }
    
    var myHashTable = new HashTable();
    console.log(myHashTable);
    
  2. 新增 set 方法,用於新增新對映

    set(key, value) {
      const index = this._hash(key);
      if (!this.dataMap[index]) this.dataMap[index] = [];
      this.dataMap[index].push({ key: key, value: value });
      return this;
    }
    
  3. 新增 get 方法,用於獲取鍵值對應的值

    get(key) {
      const index = this._hash(key);
      if (this.dataMap[index])
        for (let item of this.dataMap[index]) if (item.key === key) return item.value;
      return undefined;
    }
    
  4. 新增 keys 方法,用於獲取全部鍵

    keys() {
      let allKeys = [];
      for (let item of this.dataMap)
        if (item) for (let kv of item) allKeys.push(kv.key);
      return allKeys;
    }
    

0x03 圖

採用鄰接表實現

(1)無向圖

  1. 宣告類並實現建構函式

    class Graph {
      constructor() {
        this.adjacencyList = new Object();
      }
    }
    
    var myGraph = new Graph();
    console.log(myGraph);
    
  2. 新增 addVertex 方法,用於新增頂點

    addVertex(vertex) {
      if (!this.adjacencyList[vertex]) {
        this.adjacencyList[vertex] = [];
        return this;
      }
      throw new Error("Vertex already exists");
    }
    
  3. 新增 addEdge 方法,用於新增邊

    addEdge(vertex1, vertex2) {
      if (this.adjacencyList[vertex1] && this.adjacencyList[vertex2]) {
        this.adjacencyList[vertex1].push(vertex2);
        this.adjacencyList[vertex2].push(vertex1);
        return this;
      }
      throw new Error("Vertex not found");
    }
    
  4. 新增 removeEdge 方法,用於刪除邊

    removeEdge(vertex1, vertex2) {
      if (this.adjacencyList[vertex1] && this.adjacencyList[vertex2]) {
        this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(
          (vertex) => vertex != vertex2
        );
        this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(
          (vertex) => vertex != vertex1
        );
        return this;
      }
      throw new Error("Vertex not found");
    }
    
  5. 新增 removeVertex 方法,用於刪除頂點

    removeVertex(vertex) {
      if (this.adjacencyList[vertex]) {
        this.adjacencyList[vertex].forEach((item) => {
          this.removeEdge(vertex, item);
        });
        delete this.adjacencyList[vertex];
        return this;
      }
      throw new Error("Vertex not found");
    }
    

(2)有向圖

  1. 宣告類並實現建構函式

    class Digraph {
      constructor() {
        this.adjacencyList = new Object();
      }
    }
    
    var myDigraph = new Digraph();
    console.log(myDigraph);
    
  2. 新增 addVertex 方法,用於新增頂點

    addVertex(vertex) {
      if (!this.adjacencyList[vertex]) {
        this.adjacencyList[vertex] = [];
        return this;
      }
      throw new Error("Vertex already exists");
    }
    
  3. 新增 addEdge 方法,用於新增有向邊

    addEdge(vertexFrom, vertexTo) {
      if (this.adjacencyList[vertexFrom] && this.adjacencyList[vertexTo]) {
        this.adjacencyList[vertexFrom].push(vertexTo);
        return this;
      }
      throw new Error("Vertex not found");
    }
    
  4. 新增 removeEdge 方法,用於刪除邊

    removeEdge(vertexFrom, vertexTo) {
      if (this.adjacencyList[vertexFrom] && this.adjacencyList[vertexTo]) {
        this.adjacencyList[vertexFrom] = this.adjacencyList[vertexFrom].filter(
          (vertex) => vertex != vertexTo
        );
        return this;
      }
      throw new Error("Vertex not found");
    }
    
  5. 新增 removeVertex 方法,用於刪除頂點

    removeVertex(vertex) {
      if (this.adjacencyList[vertex]) {
        Object.keys(this.adjacencyList).forEach((key) => {
          this.removeEdge(key, vertex);
        });
        delete this.adjacencyList[vertex];
        return this;
      }
      throw new Error("Vertex not found");
    }
    

0x04 樹遍歷演算法

構造樹

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class Tree {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const newNode = new Node(value);
    if (!this.root) {
      this.root = newNode;
      return this;
    }

    let temp = this.root;
    while (true) {
      if (temp.value === value) return undefined;
      else if (temp.value > value) {
        if (!temp.left) {
          temp.left = newNode;
          return this;
        }
        temp = temp.left;
      } else {
        if (!temp.right) {
          temp.right = newNode;
          return this;
        }
        temp = temp.right;
      }
    }
  }
}

本章的演算法均作為類方法寫在 Tree 類中

(1)廣度優先搜尋 BFS

  • 廣度優先搜尋(Breadth First Search)指對於一個二叉樹進行層序遍歷

    • 舉例:有如下二叉樹

      flowchart TB 20-->15 & 26 15-->4 & 19 26-->22 & 102

      其 BFS 結果為

      flowchart LR 20-->15-->26-->4-->19-->22-->102
  • 演算法實現

    BFS() {
      let temp = this.root,
        queue = [],
        result = [];
      queue.push(temp);
      while (queue.length) {
        temp = queue.shift();
        result.push(temp.value);
        if (temp.left) queue.push(temp.left);
        if (temp.right) queue.push(temp.right);
      }
      return result;
    }
    

    測試

    var myTree = new Tree();
    myTree.insert(20);
    myTree.insert(15);
    myTree.insert(4);
    myTree.insert(19);
    myTree.insert(26);
    myTree.insert(22);
    myTree.insert(102);
    console.log(myTree.BFS());
    

(2)深度優先搜尋 DFS

  • 深度優先搜尋(Depth First Search)指對於一個二叉樹進行中序遍歷(或前序遍歷後序遍歷

    • 舉例:有如下二叉樹

      flowchart TB 20-->15 & 26 15-->4 & 19 26-->22 & 102

      其 DFS 結果為

      flowchart LR 4-->15-->19-->20-->22-->26-->102
  • 演算法實現

    DFS(order) {
      let result = [];
      const expression = [
        (node) => result.push(node.value),
        (node) => {
          if (node.left) traverse(node.left);
        },
        (node) => {
          if (node.right) traverse(node.right);
        },
      ];
      const traverse = (node) => {
        order.forEach((item) => {
          expression[item](node);
        });
      };
      traverse(this.root);
      return result;
    }
    

    測試

    var myTree = new Tree();
    myTree.insert(20);
    myTree.insert(15);
    myTree.insert(4);
    myTree.insert(19);
    myTree.insert(26);
    myTree.insert(22);
    myTree.insert(102);
    
    const Order = Object.freeze({
      PRE: [0, 1, 2],
      IN: [1, 0, 2],
      POST: [1, 2, 0],
    });
    console.log(myTree.DFS(Order.PRE));
    console.log(myTree.DFS(Order.IN));
    console.log(myTree.DFS(Order.POST));
    

0x05 排序演算法

(1)氣泡排序

從後出發,比較並交換,使最後一個值為最大值,進入下一個,重複上述操作

Array.prototype.bubble = function () {
  for (let i = this.length - 1; i > 0; i--)
    for (let j = 0; j < i; j++)
      if (this[j] > this[j + 1])
        [this[j], this[j + 1]] = [this[j + 1], this[j]];
  return this;
};

console.log([4, 2, 3, 1, 5].bubble());

(2)選擇排序

(氣泡排序的逆向)從頭出發,記錄當前位置以後的最小值的索引,比較並交換,進入下一個,重複上述操作

Array.prototype.selection = function () {
  let minIndex = this[0];
  for (let i = 0; i < this.length - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < this.length; j++)
      if (this[j] < this[minIndex]) minIndex = j;
    if (i !== minIndex) [this[i], this[minIndex]] = [this[minIndex], this[i]];
  }
  return this;
};

console.log([4, 2, 3, 1, 5].selection());

(3)插入排序

從第二項出發,找到比當前值小的值,該小值以後的值後移一位,小值後面插入到當前值,進入下一個,重複此操作

Array.prototype.insertion = function () {
  for (let i = 1; i < this.length; i++) {
    let temp = this[i];
    for (var j = i - 1; this[j] > temp && j >= 0; j--) this[j + 1] = this[j];
    this[j + 1] = temp;
  }
  return this;
};

console.log([4, 2, 3, 1, 5].insertion());

(4)合併排序

將陣列遞迴二分分組,直至每組只有兩個數字,比較並交換後逐步合併各組至原陣列大小

  1. 編寫 merge 方法,用於按大小合併兩個陣列

    const merge = (array1, array2) => {
      let i = 0,
        j = 0,
        result = [];
      while (i < array1.length && j < array2.length)
        if (array1[i] < array2[j]) result.push(array1[i++]);
        else result.push(array2[j++]);
      while (i < array1.length) result.push(array1[i++]);
      while (j < array2.length) result.push(array2[j++]);
      return result;
    };
    
  2. 結合合併方法,完成合並排序方法

    Array.prototype.merge = function () {
      if (this.length <= 1) return this;
    
      const merge = (array1, array2) => {/*...*/};
    
      let mid = Math.floor(this.length / 2),
        left = this.slice(0, mid),
        right = this.slice(mid);
      return merge(left.merge(), right.merge());
    };
    
    console.log([4, 2, 3, 1, 5].merge());
    

(5)快速排序

找到中心值,所有小於中心值的值放在中心值以左,其餘的值放在中心值以右,遞迴此操作

  1. 編寫 pivot 方法,用於獲取陣列中心值的索引

    const pivot = (array, pivotIndex, endIndex) => {
      let temp = pivotIndex;
      for (let i = pivotIndex + 1; i <= endIndex; i++)
        if (array[i] < array[pivotIndex]) {
          temp++;
          [array[temp], array[i]] = [array[i], array[temp]];
        }
      [array[pivotIndex], array[temp]] = [array[temp], array[pivotIndex]];
      return temp;
    };
    
  2. 結合中心值索引獲取方法,完成快速排序方法

    Array.prototype.quick = function (left = 0, right = this.length - 1) {
      const pivot = (pivotIndex, endIndex) => {/*...*/};
    
      if (left < right) {
        const pivotIndex = pivot(left, right);
        this.quick(left, pivotIndex - 1);
        this.quick(pivotIndex + 1, right);
      }
      return this;
    };
    

-End-

相關文章