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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章