JS實現基本數據結構封裝

介紹

常用的公式
  • 等差數列通項: an = a1+(n-1)d
  • 等差數列求和: Sn = na1+n(n-1)/2d,n∈N
  • 等比數列通項:an = a1qn-1
  • 等比數列求和:Sn = a1(1-qn)/1-q (q≠1)
數據結構
  • 考慮的問題:用什麼方式存儲組織數據,才能在使用的時候更加方便
  • 常見的數據結構:數組、棧、堆、隊列、鏈表、樹、圖、散列表
算法(Algorithm)
  • 有限指令的集合,在執行有限步驟之後終止,併產生輸出
  • 通俗理解:解決問題的辦法和步驟
算法複雜度
  • 常數操作:操作與數據量無關

  • 時間複雜度:只考慮高階項,不考慮低階項,同時忽略高階項的常數

    • 例如:冒泡排序,an²+bn+c時間複雜度爲O(n²),其中O表示整個流程收斂在n²
    • 比如二分法查找:O(log2n) 注意:默認不寫底數也是代表2爲底
  • 空間複雜度:完成操作需要的額外空間,當需要的額外空間爲常數級時爲O(1)

    • 常數級:比如與處理數據量無關的幾個變量等
  • 通常的最優解:先滿足時間複雜度最優的情況下達到空間複雜度最優

數組結構
  • 插入與刪除的效率很低
  • 查找和修改的效率很高

常見數據結構封裝

棧結構

  • 後進先出(LIFO)last in first out
  • 常見的棧操作
    • push(ele):添加一個新元素
    • pop():移除棧頂的元素,同時返回被移除的元素
    • peek():返回棧頂的元素,不對棧進行任何修改
    • isEmpty():當棧中沒有元素的時候返回true,否則返回false
    • size():返回棧裏的元素個數
    • toString():將棧結構的內容以字符的形式返回
function Stick() {
  this.items = [];

  //1.push插入一個元素
  Stick.prototype.push = function (ele) {
    this.items.push(ele);
  }

  //2.刪除棧頂元素並返回
  Stick.prototype.pop = function () {
    return this.items.pop();
  }

  //3.返回棧頂的元素,且不對棧做任何修改
  Stick.prototype.peek = function () {
    return this.items[this.items.length - 1];
  }

  //4.當棧爲空的時候返回true
  Stick.prototype.isEmpty = function () {
    return this.items.length == 0;
  }

  //5.返回棧中元素的個數
  Stick.prototype.size = function () {
    return this.items.length;
  }

  //6.以字符串的形式返回棧中元素
  Stick.prototype.toString = function () {
    var str = "";
    for (var i = 0; i < this.items.length; i++) {
      str = str + this.items[i] + " ";
    }
    return str;
  }
}

var test = new Stick();
test.push("hym");
test.push("coding");
console.log(test.pop());
console.log(test.peek());
console.log(test.isEmpty());
console.log(test.size());
console.log(test.toString());
  • 棧實現十進制轉二進制
var source = 10;
console.log(change(source));

function change(num) {
  var target = new Stack();
  var residue;
  var str = "";

  while (num > 0) {
    residue = num % 2;
    num = Math.floor(num / 2);
    target.push(residue);
  }

  while (!target.isEmpty()) {
    str = str + target.pop();
  }
  return str;
}

隊列結構

  • 先進先出(FIFO)
  • 常見的隊列操作
    • enqueue(element):向隊列尾部添加一個(或多個)新的項。
    • dequeue():移除隊列的第一個元素即排在前面的項,並返回被移除的元素。
    • front():返回隊列中第一個元素——最先被添加,也將是最先被移除的元素。隊列不做任何變動(不移除元素,只返回元素信息——與Stack類的peek方法非常類似)。
    • isEmpty():如果隊列中不包含任何元素,返回true,否則返回false
    • size():返回隊列包含的元素個數,與數組length屬性類似
// 自定義隊列
function Queue() {
    var items = []

    // 隊列操作的方法
    // enter queue方法
    this.enqueue = function (element) {
        items.push(element)
    }

    // delete queue方法
    this.dequeue = function () {
        return items.shift()
    }

    // 查看前端的元素
    this.front = function () {
        return items[0]
    }

    // 查看隊列是否爲空
    this.isEmpty = function () {
        return items.length == 0
    }

    // 查看隊列中元素的個數
    this.size = function () {
        return items.length
    }
}

優先級隊列

  • 特點:
    • 優先級隊列在插入一個元素的時候會考慮該數據的優先級.(和其他數據優先級進行比較)比較完成後, 可以得出這個元素正確的隊列中的位置.其他處理方式, 和隊列的處理方式一樣.

    • 如果我們要實現優先級隊列, 最主要是要修改添加方法. (當然, 還需要以某種方式來保存元素的優先級)

  • 優先級隊列的應用(擊鼓傳花)
//擊鼓傳花遊戲:返回最後剩下的人所在原來隊伍的位置
function playGame(arr, baseNum) {
  var team = new Queue();

  for (let i = 0; i < arr.length; i++) {
    team.enqueue(arr[i]);
  }

  while (team.size() > 1) {
    for (let j = 0; j < baseNum - 1; j++) {
      team.enqueue(team.dequeue());  //取出前面的num-1 個放在隊列後面
    }

    team.dequeue();  //將第num個刪除
  }

  var endName = team.dequeue();  //最後剩下的人
  return arr.indexOf(endName);  //最後剩下的人的位置
}

var names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl'];
var index = playGame(names, 7) // 數到8的人淘汰
console.log(index);

單向鏈表

  • 特點:

    • 存儲多個元素,元素在內存中的地址可以不連續
    • 每一個鏈表元素由一個存儲元素本身的節點和一個指向下一個元素的地址(指針、鏈接)組成
  • 相對於數組的優點:

    • 不需要連續存儲,有效利用計算機內存,實現靈活的內存動態管理
    • 不需在創建的時候規定大小,可以無限地延伸
    • 在插入或者刪除一個元素的時候,時間複雜度爲O(1)比數組的效率要高
  • 相對於數組的缺點:

    • 訪問任何一個元素的時候,都需要從頭開始遍歷(不能跳過任何一個節點)
    • 查找元素的時候不能直接通過下標查找,需要從頭開始遍歷。
  • 常用的操作

    • append(ele) 向列表尾部添加新的項
    • insert(position,ele) 向鏈表指定位置插入項
    • remove(ele) 從列表移除一個項
    • indexOf(ele) 返回元素在列表中的索引,沒有則返回-1
    • removeAt(position) 刪除指定位置的元素
    • isEmpty() 空則返回true,否則返回false
    • size() 返回鏈表包含的元素個數
    • toString() 重寫toString只輸出元素的值
function LinkList() {
  function Node(ele) {
    this.ele = ele;
    this.next = null;
  }

  this.length = 0;
  this.head = null;

  //插入節點
  LinkList.prototype.append = function (ele) {
    var newNode = new Node(ele);

    if (this.head === null) {
      this.head = newNode;
    } else {
      var cur = this.head;
      while (cur.next) {
        cur = cur.next;
      }
      cur.next = newNode;
    }

    this.length++;
  }

  //返回鏈表上元素本身的值
  LinkList.prototype.toString = function () {
    var cur = this.head;
    var str = "";

    while (cur) {
      str = str + "," + cur.ele;
      cur = cur.next;
    }

    return str.slice(1);
  }

  //在指定位置插入元素
  LinkList.prototype.insert = function (position, ele) {
    if (position < 0 || position > this.length) return false;  //判斷是否越界

    var newNode = new Node(ele);
    var cur = this.head;
    var previous = null;
    var index = 0;

    if (position == 0) {
      newNode.next = cur;
      this.head = newNode;
    } else {
      while (index++ < position) {
        previous = cur;
        cur = cur.next;
      }

      newNode.next = cur;
      previous.next = newNode;
    }

    this.length++;
    return true;
  }

  //在鏈表中查詢元素並返回相應的位置
  LinkList.prototype.indexOf = function (ele) {
    var index = 0;
    var cur = this.head;

    while (cur) {
      if (cur.ele == ele) {
        return index;
      }
      cur = cur.next;
      index++;
    }

    return -1; //沒有找到該元素時返回-1;
  }

  //移除指定位置的元素並返回元素的值
  LinkList.prototype.removeAt = function (position) {
    if (position < 0 || position > this.length) return null;  //判斷是否越界

    var cur = this.head;
    var previous = null;
    var index = 0;

    if (position == 0) {
      this.head = cur.next;
    } else {
      while (index++ < position) {
        previous = cur;
        cur = cur.next;
      }

      previous.next = cur.next;
    }

    this.length--;
    return cur.ele;
  }

  //移除指定元素
  LinkList.prototype.remove = function (ele) {
    var index = this.indexOf(ele);
    return this.removeAt(index);
  }

  //判斷鏈表是否爲空
  LinkList.prototype.isEmpty = function () {
    return this.length == 0;
  }

  //返回鏈表的長度
  LinkList.prototype.size = function () {
    return this.length;
  }

  //獲取鏈表的第一個元素
  LinkList.prototype.getFirst = function () {
    return this.head.ele;
  }
}

雙向鏈表

  • 特點:

    • 既有向前連接的引用,也有向後的連接的引用。所以既可以從頭到尾遍歷,也可以從尾到頭遍歷
    • 在插入刪除的時候需要處理四個節點的引用,比單鏈表佔內存要大
  • 常用操作和單鏈表類似

function DoublyLinkedList() {
  // 創建節點構造函數
  function Node(element) {
    this.element = element
    this.next = null
    this.prev = null
  }

  // 定義屬性
  this.length = 0
  this.head = null
  this.tail = null

  //尾部追加數據
  DoublyLinkedList.prototype.append = function (element) {
    // 根據元素創建節點
    var newNode = new Node(element)

    // 判斷列表是否爲空
    if (this.head === null) {
      this.head = newNode
      this.tail = newNode
    } else {
      this.tail.next = newNode
      newNode.prev = this.tail
      this.tail = newNode
    }
    this.length++
  }

  // 正向遍歷的方法
  DoublyLinkedList.prototype.forwardString = function () {
    var current = this.head
    var forwardStr = ""

    while (current) {
      forwardStr += "," + current.element
      current = current.next
    }
    return forwardStr.slice(1)
  }

  // 反向遍歷的方法
  DoublyLinkedList.prototype.reverseString = function () {
    var current = this.tail
    var reverseStr = ""

    while (current) {
      reverseStr += "," + current.element
      current = current.prev
    }
    return reverseStr.slice(1)
  }

  // 在任意位置插入數據
  DoublyLinkedList.prototype.insert = function (position, element) {
    if (position < 0 || position > this.length) return false   //判斷是否越界

    var newNode = new Node(element)

    if (position === 0) { // 在第一個位置插入數據
      // 判斷鏈表是否爲空
      if (this.head == null) {
        this.head = newNode
        this.tail = newNode
      } else {
        this.head.prev = newNode
        newNode.next = this.head
        this.head = newNode
      }
    } else if (position === this.length) { // 插入到最後的情況
      this.tail.next = newNode
      newNode.prev = this.tail
      this.tail = newNode
    } else { // 在中間位置插入數據
      var index = 0
      var current = this.head
      var previous = null

      while (index++ < position) {
        previous = current
        current = current.next
      }

      // 交換節點的指向順序
      newNode.next = current
      newNode.prev = previous
      current.prev = newNode
      previous.next = newNode
    }

    this.length++
    return true
  }

  // 根據位置刪除對應的元素
  DoublyLinkedList.prototype.removeAt = function (position) {
    if (position < 0 || position >= this.length) return null  //判斷是否越界

    // 判斷移除的位置
    var current = this.head
    if (position === 0) {
      if (this.length == 1) {
        this.head = null
        this.tail = null
      } else {
        this.head = this.head.next
        this.head.prev = null
      }
    } else if (position === this.length - 1) {
      current = this.tail
      this.tail = this.tail.prev
      this.tail.next = null
    } else {
      var index = 0
      var previous = null

      while (index++ < position) {
        previous = current
        current = current.next
      }

      previous.next = current.next
      current.next.prev = previous
    }

    this.length--

    return current.element
  }

  // 根據元素獲取在鏈表中的位置
  DoublyLinkedList.prototype.indexOf = function (element) {
    var current = this.head
    var index = 0

    while (current) {
      if (current.element === element) {
        return index
      }
      index++
      current = current.next
    }
    return -1
  }

  // 根據元素刪除
  DoublyLinkedList.prototype.remove = function (element) {
    var index = this.indexOf(element)
    return this.removeAt(index)
  }

  // 判斷是否爲空
  DoublyLinkedList.prototype.isEmpty = function () {
    return this.length === 0
  }

  // 獲取鏈表長度
  DoublyLinkedList.prototype.size = function () {
    return this.length
  }

  // 獲取第一個元素
  DoublyLinkedList.prototype.getHead = function () {
    return this.head.element
  }

  // 獲取最後一個元素
  DoublyLinkedList.prototype.getTail = function () {
    return this.tail.element
  }
}

集合結構

  • 特點:

    • 集合通常是由一組無序的, 不能重複的元素構成(沒有順序意味着不能通過下標值進行訪問, 不能重複意味着相同的對象在集合中只會存在一份.)
  • 集合中常用的操作方法

    • add(value):向集合添加一個新的項。
    • remove(value):從集合移除一個值。
    • has(value):如果值在集合中,返回true,否則返回false。
    • clear():移除集合中的所有項。
    • size():返回集合所包含元素的數量。與數組的length屬性類似。
    • values():返回一個包含集合中所有值的數組。
// 封裝集合的構造函數
function Set() {
    // 使用一個對象來保存集合的元素
    this.items = {}

    // 集合的操作方法
    // 判斷集合中是否有某個元素
    Set.prototype.has = function (value) {
        return this.items.hasOwnProperty(value)
    }

    // 向集合中添加元素
    Set.prototype.add = function (value) {
        // 1.判斷集合中是否已經包含了該元素
        if (this.has(value)) return false

        // 2.將元素添加到集合中
        this.items[value] = value
        return true
    }

    // 從集合中刪除某個元素
    Set.prototype.remove = function (value) {
        // 1.判斷集合中是否包含該元素
        if (!this.has(value)) return false

        // 2.包含該元素, 那麼將元素刪除
        delete this.items[value]
        return true
    }

    // 清空集合中所有的元素
    Set.prototype.clear = function () {
        this.items = {}
    }

    // 獲取集合的大小
    Set.prototype.size = function () {
        return Object.keys(this.items).length

        /*
        考慮兼容性問題, 使用下面的代碼
        var count = 0
        for (var value in this.items) {
            if (this.items.hasOwnProperty(value)) {
                count++
            }
        }
        return count
        */
    }

    // 獲取集合中所有的值
    Set.prototype.values = function () {
        return Object.keys(this.items)

        /*
        考慮兼容性問題, 使用下面的代碼
        var keys = []
        for (var value in this.items) {
            keys.push(value)
        }
        return keys
        */
    }
}

字典結構

  • 特點

    • 一一對應關係
    • 使用字典的方式: {“age” : 18, “name” : “Coderwhy”, “height”: 1.88} 可以通過key值訪問value
  • 字典常用操作

    • set(key,value):向字典中添加新元素。
    • remove(key):通過使用鍵值來從字典中移除鍵值對應的數據值。
    • has(key):如果某個鍵值存在於這個字典中,則返回true,反之則返回false。
    • get(key):通過鍵值查找特定的數值並返回。
    • clear():將這個字典中的所有元素全部刪除。
    • size():返回字典所包含元素的數量。與數組的length屬性類似。
    • keys():將字典所包含的所有鍵名以數組形式返回。
    • values():將字典所包含的所有數值以數組形式返回。
function Dictionary() {
  this.items = {};

  Dictionary.prototype.set = function (key, value) {
    this.items[key] = value;
  }

  Dictionary.prototype.has = function (key) {
    return this.items.hasOwnProperty(key);
  }

  Dictionary.prototype.remove = function (key) {
    if (!this.has(key)) {
      return false;
    }

    delete this.items[key];
    return true;
  }

  Dictionary.prototype.get = function (key) {
    return this.has(key) ? this.items[key] : undefined;
  }

  Dictionary.prototype.getKeys = function () {
    return Object.keys(this.items);
  }

  Dictionary.prototype.getValues = function () {
    return Object.values(this.items);
  }

  Dictionary.prototype.getSize = function () {
    return this.getKeys().length
  }

  Dictionary.prototype.clear = function () {
    this.items = {}
  }
}

哈希表

  • 特點:

    • 可以快速插入、刪除、查找
    • 插入、刪除的時間複雜度接近於常數級O(1)
    • 查找速度比樹還要快
    • 數據沒有順序,不能像數組那樣按固定的方式(從小到大)遍歷元素
    • 通常情況,key值不能重複
  • 解決地址衝突的辦法

    • 鏈地址法(每個數組單元中存儲的不再是單個數據, 而是一個鏈條,鏈條可以是數組或者鏈表)
    • 開放地址法(尋找空白的單元格來添加重複的數據,尋找的方法有線性探索、二次探索、再次哈希法)
  • 裝填因子

    • 表示在哈希表中已包含的數據項和整個哈希表長度的比值:裝填因子 = 總數據項/哈希表的長度,最大值爲1
  • 總結:鏈地址法的效率比開放地址法的效率要高,不會因爲添加某元素後性能就急劇下降(在java的HashMap中使用的就是鏈地址法)

  • 哈希表擴容(在添加完新數據時判斷)

    • 雖然使用的是鏈地址法,可以無限制插入,但數據量增多會導致每個Index對應的bucket越長,造成效率降低,所以可以對數組進行適當擴容
    • 擴容需要對所有數據項進行哈希化,來獲取到不同的位置
    • 一般在loadFactor>0.75的時候進行擴容,比如Java的哈希表。同樣當刪除數據時,loadFactor<0.25時將數量限制在一半
// 創建HashTable構造函數
function HashTable() {
    // 定義屬性
    this.storage = []
    this.count = 0
    this.limit = 8

    // 定義相關方法
    // 判斷是否是質數
    HashTable.prototype.isPrime = function (num) {
        var temp = parseInt(Math.sqrt(num))
        // 2.循環判斷
        for (var i = 2; i <= temp; i++) {
            if (num % i == 0) {
                return false
            }
        }
        return true
    }

    // 獲取質數
    HashTable.prototype.getPrime = function (num) {
        while (!isPrime(num)) {
            num++
        }
        return num
    }

    // 哈希函數
    HashTable.prototype.hashFunc = function(str, max) {
        // 1.初始化hashCode的值
        var hashCode = 0

        // 2.霍納算法, 來計算hashCode的數值
        for (var i = 0; i < str.length; i++) {
            hashCode = 37 * hashCode + str.charCodeAt(i)
        }

        // 3.取模運算
        hashCode = hashCode % max
        return hashCode
    }

    // 插入數據方法
    HashTable.prototype.put = function (key, value) {
        // 1.獲取key對應的index
        var index = this.hashFunc(key, this.limit)

        // 2.取出數組(也可以使用鏈表)
        // 數組中放置數據的方式: [[ [k,v], [k,v], [k,v] ] , [ [k,v], [k,v] ]  [ [k,v] ] ]
        var bucket = this.storage[index]

        // 3.判斷這個數組是否存在
        if (bucket === undefined) {
            // 3.1創建桶
            bucket = []
            this.storage[index] = bucket
        }

        // 4.判斷是新增還是修改原來的值.
        var override = false
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            if (tuple[0] === key) {
                tuple[1] = value
                override = true
            }
        }

        // 5.如果是新增, 前一步沒有覆蓋
        if (!override) {
            bucket.push([key, value])
            this.count++

            if (this.count > this.limit * 0.75) {
                var primeNum = this.getPrime(this.limit * 2)
                this.resize(primeNum)
            }
        }
    }

    // 獲取存放的數據
    HashTable.prototype.get = function (key) {
        // 1.獲取key對應的index
        var index = this.hashFunc(key, this.limit)

        // 2.獲取對應的bucket
        var bucket = this.storage[index]

        // 3.如果bucket爲null, 那麼說明這個位置沒有數據
        if (bucket == null) {
            return null
        }

        // 4.有bucket, 判斷是否有對應的key
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            if (tuple[0] === key) {
                return tuple[1]
            }
        }

        // 5.沒有找到, return null
        return null
    }

    // 刪除數據
    HashTable.prototype.remove = function (key) {
        // 1.獲取key對應的index
        var index = this.hashFunc(key, this.limit)

        // 2.獲取對應的bucket
        var bucket = this.storage[index]

        // 3.判斷同是否爲null, 爲null則說明沒有對應的數據
        if (bucket == null) {
            return null
        }

        // 4.遍歷bucket, 尋找對應的數據
        for (var i = 0; i < bucket.length; i++) {
            var tuple = bucket[i]
            if (tuple[0] === key) {
                bucket.splice(i, 1)
                this.count--

                // 縮小數組的容量
                if (this.limit > 7 && this.count < this.limit * 0.25) {
                    var primeNum = this.getPrime(Math.floor(this.limit / 2))
                    this.resize(primeNum)
                }
            }
            return tuple[1]
        }

        // 5.來到該位置, 說明沒有對應的數據, 那麼返回null
        return null
    }

    // isEmpty方法
    HashTable.prototype.isEmpty = function () {
        return this.count == 0
    }

    // size方法
    HashTable.prototype.size = function () {
        return this.count
    }

    // 哈希表擴容
    HashTable.prototype.resize = function (newLimit) {
        // 1.保存舊的數組內容
        var oldStorage = this.storage

        // 2.重置屬性
        this.limit = newLimit
        this.count = 0
        this.storage = []

        // 3.遍歷舊數組中的所有數據項, 並且重新插入到哈希表中
        oldStorage.forEach(function (bucket) {
            // 1.bucket爲null, 說明這裏面沒有數據
            if (bucket == null) {
                return
            }

            // 2.bucket中有數據, 那麼將裏面的數據重新哈希化插入
            for (var i = 0; i < bucket.length; i++) {
                var tuple = bucket[i]
                this.put(tuple[0], tuple[1])
            }
        }).bind(this)
    }
}

樹結構

  • 術語

    • 結點的度:結點的子樹個數
    • 樹的度:樹所有結點中最大的度數
    • 葉結點:度爲0的結點
    • 路徑和路徑長度:路徑爲結點序列,其中路徑包含邊的個數爲路徑的長度
    • 結點層次:規定根結點爲1層,任一結點層次是父結點層次加一
    • 樹的深度:樹中所有結點的最大層次
  • 二叉樹

    • 每個結點最多只能有兩個子結點,且二叉樹可以爲空
    • 一個二叉樹第 i 層的最大結點數爲:2^(i-1), i >= 1;
    • 深度爲k的二叉樹有最大結點總數爲: 2^k - 1, k >= 1;
    • 葉子結點總數n0是度爲2的非葉子結點個數n2加一 n0 = n2+1
    • 滿二叉樹(完美二叉樹):除了葉子結點外,每層結點都有兩個子結點
    • 完全二叉樹:除了二叉樹最後一層外,其他各層結點數都達到最大,且最後一層的葉子結點從左往右連續存在
  • 二叉搜索樹

    • 二叉搜索樹也稱爲二叉排序樹或二叉查找樹,可以爲空,不爲空時需要滿足如下性質:
      • 非空左子樹的所有鍵值小於其根結點的鍵值
      • 非空右子樹的所有鍵值大於其根結點的鍵值
      • 左、右子樹本身也都是二叉搜索樹
  • 二叉搜索樹刪除結點

    • 情況一:沒有子結點
      • 檢測current的left和rigt是否都是null
      • 都爲null之後檢查current是否是根,都爲null且爲根接地那時候沒救相當於清空二叉樹
      • 否則,把父級結點的left或right字段設置爲null
    • 情況二:只有一個子結點
      • 要刪除的current結點,只有2個連接,一個連接父結點,一個連接唯一的子結點
      • 需要從這三者之間: 爺爺 - 自己 - 兒子, 將自己(current)剪短, 讓爺爺直接連接兒子即可.
      • 這個過程要求改變父節點的left或者right, 指向要刪除節點的子節點
      • 在這個過程中還要考慮是否current就是根
    • 情況三:兩個子結點
      • 從子節點中尋找最接近結點值的結點,來替換當前結點。這個結點要麼是小一點點的current左子樹的最大值(前驅),要麼是大一點點的current右子樹的最小值(後繼)
//二叉搜索樹
function BinarySearchTree() {
  //根結點
  this.root = null;

  //創建結點的構造函數
  function Node(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }

  // 插入結點 參數:插入結點的key值
  BinarySearchTree.prototype.insert = function (key) {
    var newNode = new Node(key);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  BinarySearchTree.prototype.insertNode = function (node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  // 前序遍歷 參數:對結點key值的處理函數
  BinarySearchTree.prototype.preOrderTraversal = function (handler) {
    this.preOrderTraversalNode(this.root, handler);
  }

  BinarySearchTree.prototype.preOrderTraversalNode = function (node, handler) {
    if (node !== null) {
      handler(node.key);  //打印當前結點
      this.preOrderTraversalNode(node.left, handler);
      this.preOrderTraversalNode(node.right, handler);
    }
  }

  //中序遍歷 
  BinarySearchTree.prototype.inOrderTraversal = function (handler) {
    this.inOrderTraversalNode(this.root, handler);
  }

  BinarySearchTree.prototype.inOrderTraversalNode = function (node, handler) {
    if (node !== null) {
      this.inOrderTraversalNode(node.left, handler);
      handler(node.key);
      this.inOrderTraversalNode(node.right, handler);
    }
  }

  //後序遍歷
  BinarySearchTree.prototype.postOrderTraversal = function (handler) {
    this.postOrderTraversalNode(this.root, handler);
  }

  BinarySearchTree.prototype.postOrderTraversalNode = function (node, handler) {
    if (node !== null) {
      this.postOrderTraversalNode(node.left, handler);
      this.postOrderTraversalNode(node.right, handler);
      handler(node.key);
    }
  }

  //獲取最小值
  BinarySearchTree.prototype.min = function () {
    var node = this.root;
    while (node.left !== null) {
      node = node.left;
    }
    return node.key;
  }

  //獲取最大值
  BinarySearchTree.prototype.max = function () {
    var node = this.root;
    while (node.right !== null) {
      node = node.right;
    }
    return node.key;
  }

  //搜索特定的值,返回true或者false
  BinarySearchTree.prototype.search = function (key) {
    return this.searchNode(this.root, key);
  }

  BinarySearchTree.prototype.searchNode = function (node, key) {
    if (node == null) {
      return false;
    }

    if (node.key > key) {
      return this.searchNode(node.left, key);
    } else if (node.key < key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  //非遞歸搜索
  /*  BinarySearchTree.prototype.search = function (key) {
     var node = this.root;
     while (node !== null) {
       if (node.key > key) {
         node = node.left;
       } else if (node.key < key) {
         node = node.right;
       } else {
         return true;
       }
     }
     return false;
   } */


  //二叉搜索樹刪除結點
  BinarySearchTree.prototype.remove = function (key) {
    var current = this.root;
    var parent = this.root;
    var isLeftChild = true;

    //查找結點
    while (current.key !== key) {
      parent = current;
      if (key < current.key) {
        isLeftChild = true;
        current = current.left;
      } else {
        isLeftChild = false;
        current = current.right;
      }

      if (current === null) {
        return false;  //沒有找到要刪除的結點
      }
    }

    //情況一:刪除的是葉子結點
    if (current.left === null && current.right === null) {
      if (current == this.root) {
        this.root = null;
      } else if (isLeftChild) {
        parent.left = null;
      } else {
        parent.right = null;
      }
    } else if (current.right === null) { //情況二: 刪除有一個子結點的結點
      if (current == this.root) {
        this.root = current.left;
      } else if (isLeftChild) {
        parent.left = current.left;
      } else {
        parent.right = current.right;
      }
    } else if (current.left === null) {
      if (current == this.root) {
        this.root = current.right;
      } else if (isLeftChild) {
        parent.left = current.right;
      } else {
        parent.right = current.right;
      }
    } else {   //情況三:刪除有兩個結點的結點
      //獲取後繼結點
      var successor = this.getSuccessor(current);

      if (current == this.root) {
        this.root = successor;
      } else if (isLeftChild) {
        parent.left = successor;
      } else {
        parent.right = successor;
      }

      successor.left = current.left;
    }
    return true;
  }

  //尋找後繼結點(在右子樹中尋找最小值)
  BinarySearchTree.prototype.getSuccessor = function (delNode) {
    var successorParent = delNode;
    var successor = delNode;
    var current = delNode.right;

    //在左子樹中找最小值,即最後一個左結點
    while (current !== null) {
      successorParent = successor;
      successor = current;
      current = current.left;
    }

    if (successor != delNode.right) {
      successorParent.left = successor.right;   //摘結點successor
      successor.right = delNode.right;
    }
    return successor;
  }
}

// 測試代碼
var test = new BinarySearchTree()

// 插入數據
test.insert(11)
test.insert(7)
test.insert(15)

紅黑樹

當插入連續的數據之後二叉搜索樹的分佈會很不均勻,稱之爲非平衡樹,相當於編寫了一個鏈表,查找效率變成O(N);
而對於平衡二叉樹(每個結點左邊的子孫結點的個數儘可能等於右邊),插入/查找等操作的效率是O(logN)

  • AVL樹
    • AVL是最早的一種平衡樹,通過每個結點多存儲一個額外數據來保持樹的平衡
    • 因爲AVL樹是平衡的,所以時間複雜度也是O(logN)
    • 但是每次插入/刪除操作相對紅黑樹效率都不高,所以整體效率不如紅黑樹

  • 紅黑樹,除了符合二叉搜索樹的基本規則之外,還添加了以下特性:
    • 結點是紅色或者黑色
    • 根結點是黑色
    • 每個葉子結點都是黑色的空結點
    • 每個紅色結點的兩個子結點都是黑色(即從每個葉子到根的所有路徑上不能有兩個連續的紅色結點)
    • 從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點

  • 紅黑樹的上述約定確保了以下的關鍵特性
    • 從根到葉子的最長可能路徑,不會超過最短可能路徑的兩倍長
      • 路徑不能有兩個相連的紅色結點
      • 最短的可能路徑都是黑色結點
      • 最長的可能路徑是紅色和黑色交替
      • 性質5 所有路徑都有相同數目的黑色結點
    • 結果就是這個樹基本是平衡的
    • 雖然沒有做到絕對平衡,但是可以保證在最壞的情況下,依然是高效的

  • 紅黑樹的變換
    • 插入新結點時,有可能樹不再平衡,可以通過 換色 - 左旋轉 - 右旋轉 讓樹保持平衡
    • 通常插入的新結點都是紅色,如果出現紅紅相連的情況,就進行顏色調換和旋轉
    • 左旋轉:逆時針旋轉兩個結點,使得結點被自己的右孩子取代,而自己成爲左孩子
    • 右旋轉:順時針旋轉紅黑樹的兩個結點,使得父結點被自己的左孩子取代,自己成爲右孩子

詳細可以在網址瞭解

圖結構

  • 相關術語

    • 相鄰頂點:由一條邊連接在一起的頂點稱爲相鄰頂點
    • 度:一個頂點的度是相鄰頂點的數量
    • 路徑:路徑是頂點v1, v2…, vn的一個連續序列
    • 無向圖:所有的邊都沒有方向
    • 有向圖:有向圖表示的圖中的邊是有方向的
    • 無權圖:圖中的邊是沒有任何意義的(邊沒有攜帶權重)
    • 帶權圖:邊有一定的權重(可以是任意你希望表示的數據: 比如距離或者花費的時間或者票價)
  • 圖的表示

    • 鄰接矩陣(讓每個結點和一個整數相關聯,當兩個結點之間有邊則在二維數組的值爲1,否則爲0)
    • 鄰接表:由圖中每個頂點以及和頂點相鄰的頂點列表組成(列表可以用數組、鏈表、哈希表存儲)
  • 圖的遍歷

    • 廣度優先搜索BFS(Breadth-First Search):基於隊列,先入隊列的先被探索(先訪問相鄰點)
    • 深度優先搜索DFS(Depth-First Search):基於棧(沿路徑訪問)
    • 爲了記錄頂點是否被訪問過,用三種顏色表示狀態
      • 白色:表示頂點沒有被訪問過
      • 灰色:表示該頂點被訪問過,但並未被探索過
      • 黑色:表示該頂點被訪問過且被完全探索過
//用鄰接表存儲圖結構
function Grapha() {
  this.vertexes = []; //存儲圖的頂點
  this.adjList = new Dictionary();  //addjoin存儲邊

  //添加頂點
  Grapha.prototype.addVertex = function (v) {
    this.vertexes.push(v);
    this.adjList.set(v, []);   //用數組存儲連接頂點的邊
  }

  //添加邊
  Grapha.prototype.addEdge = function (v, w) {
    this.adjList.get(v).push(w);
    this.adjList.get(w).push(v);   //注意要給兩個頂點都添加,邊是連接兩個頂點的
  }

  //顯示圖的結構
  Grapha.prototype.toString = function () {
    var resultStr = "";
    for (var i = 0; i < this.vertexes.length; i++) {
      resultStr += this.vertexes[i] + "->";

      var adj = this.adjList.get(this.vertexes[i]);
      for (var j = 0; j < adj.length; j++) {
        resultStr += adj[j] + " ";
      }
      resultStr += "\n";
    }

    return resultStr;
  }

  //初始化頂點訪問狀態
  Grapha.prototype.initializeColor = function () {
    var colors = [];
    for (var i = 0; i < this.vertexes.length; i++) {
      colors[this.vertexes[i]] = "white";
    }
    return colors;
  }

  //廣度優先遍歷
  Grapha.prototype.bfs = function (v, handler) {
    var color = this.initializeColor();
    var queue = new Queue();
    queue.enqueue(v);

    while (!queue.isEmpty()) {
      var qv = queue.dequeue();   //從隊列中取出頂點
      var qAdj = this.adjList.get(qv);   //獲取到qv的所有相鄰結點
      color[qv] = "gray";   //標記爲已經訪問,但未探索

      for (var i = 0; i < qAdj.length; i++) {
        var a = qAdj[i];
        if (color[a] === "white") {
          queue.enqueue(a);
          color[a] = "gray";
        }
      }

      color[qv] = "black";   //將qv標記爲探測完畢

      if (handler) {
        handler(qv);   //處理qv
      }
    }
  }

  //深度優先遍歷
  Grapha.prototype.dfs = function (handler) {
    var color = this.initializeColor();

    for (var i = 0; i < this.vertexes.length; i++) {
      if (color[this.vertexes[i]] === "white") {
        this.dfsVisit(this.vertexes[i], color, handler);
      }
    }
  }

  //遞歸調用方法
  Grapha.prototype.dfsVisit = function (u, color, handler) {
    color[u] = "gray";
    if (handler) {
      handler(u);
    }

    var uAdj = this.adjList.get(u);
    for (var i = 0; i < uAdj.length; i++) {
      var w = uAdj[i];
      if (color[w] === "white") {
        this.dfsVisit(w, color, handler);
      }
    }

    color[u] = "black";
  }
}

//測試
var grapha = new Grapha();
var v = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];

v.forEach((item) => {
  grapha.addVertex(item);
})

// 添加邊
grapha.addEdge('A', 'B');
grapha.addEdge('A', 'C');
grapha.addEdge('A', 'D');
grapha.addEdge('C', 'D');
grapha.addEdge('C', 'G');
grapha.addEdge('D', 'G');
grapha.addEdge('D', 'H');
grapha.addEdge('B', 'E');
grapha.addEdge('B', 'F');
grapha.addEdge('E', 'I');


// 調用廣度優先算法
var result = ""
grapha.dfs(function (v) {
  result += v + " "
})
console.log(result) // A B E I F C D G H 

發佈了54 篇原創文章 · 獲贊 13 · 訪問量 9145
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章