数据结构与算法

66 阅读29分钟

常见的数据结构有哪些?

队列(queue)、树(tree)、堆(heap)、数组(Array)、栈(stack)、链表(linked list)、图(graph)、散列表(hash)

算法的定义(algorithm)

# 一个有限指令集,每条指令的描述不依赖于语言
# 接收一个输入(有些不需要输入)
# 产生输出
# 一定在有限步骤之后终止

algorithm的本意是解决问题的办法/步骤逻辑
数据结构的实现,离不开算法

数组 Array

数组结构的优点是查找和替换快(只需要提供索引就可以快速的找到对应的元素或将其替换),但是新增或删除时效率很低,因为新增或删除会影响后面所有元素的索引。

栈结构 stack

栈结构是一种受限的线性结构,只有一个口,只能在一端添加和删除元素,遵守先进后出原则(FILO first in last out)。
应用场景:函数调用栈
封装栈(可以用数组和链表封装,这里用数组封装)

// 封装栈类
  function Stack() {
    // 栈中的属性
    this.items = [];
    // this.push = function() {}  是在每个类实例添加一个方法,不共享,而在原型上添加方法是共享的,可节省内存
    // 栈的操作
    // 1、将元素压入栈
    Stack.prototype.push = function (element) {
      this.items.push(element);
    }
    // 2、取出栈顶元素
    Stack.prototype.pop = function () {
      return this.items.pop();
    }
    // 3、查看栈顶元素
    Stack.prototype.peek = function () {
      return this.items[this.items.length - 1];
    }
    // 4、查看栈是否为空
    Stack.prototype.isEmpty = function () {
      return this.items.length === 0;
    }
    // 5、返回栈元素个数
    Stack.prototype.size = function () {
      return this.items.length;
    }
    // 6、将栈结构的内容以字符串的形式返回
    Stack.prototype.toString = function () {
      return this.items.toString();
    }
  }
  
  // 栈的使用
  var s = new Stack();
  s.push(1);
  s.push(2);
  s.push(3);
  console.log(s.pop());
  console.log(s.peek());
  console.log(s.isEmpty());
  console.log(s.size());
  console.log(s.toString());

利用栈将十进制转换为二进制

计算机底层都是用二进制来控制的,通过电路的开和关来控制很多东西。汇编语言是对机器语言(0/1)的抽象,高级语言是对汇编语言的抽象。越高级越接近人类的语言。但是最终我们的语言都会转成二进制去执行。

// 将十进制转换为二进制
function dec2bin(decNumber) {
    // 1、定义栈对象
    var stack = new Stack();
    // 2、循环操作
    while(decNumber > 0) {
      // 2.1 获取余数并且放入栈中
      stack.push(decNumber % 2);
      // 2.2 获取整除后的结果作为下一次运行的数字
      decNumber = Math.floor(decNumber / 2);
    }
    // 3、从栈中取出0和1
    var binaryString = '';
    while(!stack.isEmpty()) {
      binaryString += stack.pop();
    }
    return binaryString;
}
console.log(dec2bin(100)); // 1100100

队列 queue

队列是一种受限的线性结构,先进先出(FIFO first in first out)。在一端添加在另一端删除元素。\

封装队列

  // 封装队列类
  // 还是按照基于对象的方式
  function Queue() {
    // 属性
    this.items = [];
    // 方法
    // 1、向队列尾部添加一个或多个项
    Queue.prototype.enqueue = function (element) {
      this.items.push(element);
    }
    // 2、移除队列的第一项(排在队列最前面的),并返回移除的项
    Queue.prototype.dequeue = function () {
      return this.items.shift();
    }
    // 3、返回队列的第一项(最前面的一项),也将是最先被移除的元素,只返回,不移除任何元素,类似于栈的peek方法
    Queue.prototype.front = function () {
      return this.items[0];
    }
    // 4、队列是否为空
    Queue.prototype.isEmpty = function () {
      return this.items.length === 0;
    }
    // 5、队列包含元素的个数
    Queue.prototype.size = function () {
      return this.items.length;
    }
    // 6、将队列转换成字符串形式
    Queue.prototype.toString = function () {
      return this.items.toString();
    }
  }
  // 使用队列
  var queue = new Queue();
  queue.enqueue(1);
  queue.enqueue(2);
  queue.enqueue(3);
  queue.dequeue();
  console.log(queue.front())
  console.log(queue.isEmpty())
  console.log(queue.size())
  console.log(queue.toString())

用队列实现击鼓传花

  // 击鼓传花
  // 若干个人围成一圈,循环报数,报到最后一个数就淘汰,求出最后胜出的人
  function passGame(nameList, num) {
    // 1、创建一个队列结构
    var queue  = new  Queue();
    // 2、将所有人依次假如队列中
    for(let i = 0; i < nameList.length; i ++) {
      queue.enqueue(nameList[i]);
    }
    // 3、开始数数
    while(queue.size() > 1) {
      // 不是num时重新加入队列的末尾
      // 是num时,将其从队列中删除
      // 3.1 num数字之前的人重新放到队列内
      for(let i = 0; i < num - 1; i++) {
        queue.enqueue(queue.dequeue());
      }
      // 3.2 num数字的人从队列删除
      queue.dequeue();
    }
    // 查看最后胜出者
    return queue.front();
  }
  console.log(passGame(['tom', 'bob', 'july'], 3)); // bob

优先级队列

优先级队列和队列相比,在插入数据时,不会直接插入到后端,而是根据数据的优先级插入到对应的位置。

  // 封账优先级队列
  function PriorityQueue() {
    // 内部类
    function QueueElement(element, priority) {
      this.element = element;
      this.priority = priority;
    }
    // 封装属性
    this.items = [];
    // 1、实现插入方法
    PriorityQueue.prototype.enQueue = function(element, priority) {
      // 1、创建QueueElement
      var queueElement = new QueueElement(element, priority);
      // 2、判断队列是否为空
      if(this.items.length === 0) {
        this.items.push(queueElement);
      } else {
        var added = false;
        for(let i = 0; i < this.items.length; i++) {
          if(queueElement.priority < this.items[i].priority) {
            this.items.splice(i, 0, queueElement);
            added = true;
            break;
          }
        }
        if(!added) {
          this.items.push(queueElement);
        }
      }
    }
    // 2、移除队列的第一项(排在队列最前面的),并返回移除的项
    PriorityQueue.prototype.dequeue = function () {
      return this.items.shift();
    }
    // 3、返回队列的第一项(最前面的一项),也将是最先被移除的元素,只返回,不移除任何元素,类似于栈的peek方法
    PriorityQueue.prototype.front = function () {
      return this.items[0];
    }
    // 4、队列是否为空
    PriorityQueue.prototype.isEmpty = function () {
      return this.items.length === 0;
    }
    // 5、队列包含元素的个数
    PriorityQueue.prototype.size = function () {
      return this.items.length;
    }
    // 6、将队列转换成字符串形式
    PriorityQueue.prototype.toString = function () {
      return this.items.toString();
    }
  }

  // 测试代码
  var pq = new PriorityQueue();
  pq.enQueue('tom', 10);
  pq.enQueue('july', 5);
  pq.enQueue('bob', 5);
  pq.enQueue('tony', 1);
  pq.enQueue('jery', 1000);
  console.log(pq.items)

链表

链表也是线性存储结构,不同于数组的是,链表的元素在内存中不必是连续的空间。链表的每一个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针)组成。
链表相对于数组的优势:
1、元素在内存中不必是连续的空间,实现灵活的内存动态管理。
2、链表不必在创建时就确定内存大小。并且大小可以无限延伸下去。
3、链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组的插入和删除效率高很多。

相对于数组,链表有一些缺点:
1、链表访问任何一个位置的元素时都需要从头开始访问(无法跳过第一个元素访问任何一个元素)。
2、无法通过下表直接访问元素,需要从头一个个访问,直到找到对应的元素。

链表是什么?
链表像一个火车,有一个火车头,火车头会连接一个节点,节点上有乘客(类似数据),并且这个节点会连接下一个节点。

image.png

单向链表封装

  // 链表封装
  function LinkedList() {
    // 内部类:节点类
    function Node(data) {
      this.data = data;
      this.next  =null;
    }
    // 属性
    this.head = null;
    this.length = 0;
    // 方法
    // 向链表尾部添加新项
    LinkedList.prototype.append = function(data) {
      // 1.创建新节点
      var newNode = new Node(data);
      // 2.判断是否添加的是第一个节点
      if(this.length === 0) { // 2.1 第一个节点
        this.head = newNode;
      } else {                // 2.2 不是第一个节点
        // 找到最后一个节点
        var current = this.head;
        while(current.next) {
          current = current.next;
        }
        // 最后节点的next指向新节点
        current.next = newNode;
      }
      // 3.长度+1
      this.length += 1;
    }
    // 向指定位置插入新项
    LinkedList.prototype.insert = function(position, data) {
      // 1.对position进行越界判断
      if(position < 0 || position > this.length) return false;
      // 2.创建新节点
      var newNode = new Node(data);
      // 3.判断插入位置是否是第一个
      if(position === 0) { // 是第一个节点
        // 让新节点的指针指向原来的第一个
        newNode.next = this.head;
        // 第一个节点指向新节点
        this.head = newNode;
      } else { // 不是第一个节点
        // 找到position对应的节点
        var index = 0;
        var current = this.head;
        var previous = null; // current的前一个节点
        while(index++ < position) {
          previous = current;
          current = current.next;
        }
        // 让新节点的next指向position对应的节点
        newNode.next = current;
        // 让current的前一个节点指向新节点
        previous.next = newNode;
      }
      // 长度+1
      this.length += 1;
      return true;
    }
    // 获取对应位置的元素
    LinkedList.prototype.get = function(position) {
      // 1.越界判断
      if(position < 0 || position >= this.length) return null;
      // 2.获取对应的data
      var current = this.head;
      var index = 0;
      while(index++ < position) {
        current = current.next;
      }
      return current.data;
    }
    // 返回元素在链表中的索引,没有则返回-1
    LinkedList.prototype.indexOf = function(data) {
      // 1.定义变量
      var current = this.head;
      var index = 0;
      // 2.开始查找
      while(current) {
        if(current.data === data) {
          return index;
        }
        index++;
        current = current.next;
      }
      // 3.找到最后没有找到,返回-1
      return -1;
    }
    // 修改某个位置的元素
    LinkedList.prototype.update = function(position,newData) {
      // 1.越界判断
      if(position < 0 || position >= this.length) return false;
      // 2.查找正确的节点
      var current = this.head;
      var index = 0;
      while(index++ < position) {
        current = current.next;
      }
      // 3.将position位置的节点的data修改成newData
      current.data = newData;
      return true;
    }
    // 从链表中的特定位置移除一项
    LinkedList.prototype.removeAt = function(position) {
      // 1.越界判断
      if(position < 0 || position >= this.length) return null;
      var current = this.head;
      // 判断是否删除的是第一个节点
      if(position === 0) {
        this.head = this.head.next;
      } else {
        var previous = null; // 当前节点的前一个节点
        var index = 0;
        while(index++ < position) {
          previous = current;
          current = current.next;
        }
        // 让前一个节点的next指向current的next
        previous.next = current.next;
      }
      // 长度-1
      this.length -= 1;
      return current.data;
    }
    // 从链表中移除一项
    LinkedList.prototype.remove = function(data) {
      // 1.获取data在列表中的位置
      var position = this.indexOf(data);
      // 2.根据位置信息删除节点
      return this.removeAt(position);
    }
    // 链表是否为空
    LinkedList.prototype.isEmpty = function() {
      return this.length === 0;
    }
    // 链表中包含元素的个数
    LinkedList.prototype.size = function() {
      return this.length;
    }
    // 以字符串形式输出链表
    LinkedList.prototype.toString = function() {
      // 1.定义变量(指针)
      var current = this.head;
      var listString = '';
      // 2.循环获取一个个的节点
      while(current) {
        listString += current.data + ' ';
        current = current.next;
      }
      return listString;
    }
  }
  // 测试代码
  // 1.创建链表
  var list = new LinkedList();
  // 2.测试追加方法
  list.append(1);
  list.append(2);
  list.append(3);
  // 3.测试插入方法
  list.insert(3, 4);
  console.log(list.toString());
  console.log(list)
  // 4.测试查找方法
  console.log(list.get(2))
  // 5.测试indexOf方法
  console.log(list.indexOf(1));
  console.log(list.indexOf(3));
  console.log(list.indexOf(10));
  // 测试update方法
  list.update(0, 'tom');
  list.update(5, '无效');
  console.log(list.get(0))
  console.log(list.get(5))
  // 测试removeAt方法
  console.log(list.removeAt(0));
  // 测试remove方法
  list.remove(2);
  console.log(list.toString());
  // 测试isEmpty和size方法
  console.log(list.isEmpty());
  console.log(list.size());

双向链表

双向链表比单向链表多了一个指向前一个节点的引用。所以双向链表还支持向前遍历。而单向链表只能从头开始遍历。双向链表插入和删除时会关系到4个引用,实现起来比较复杂,占用的内存空间会比单向链表大,但是这些缺点和使用方便相比,微不足道。(文本编辑器使用到双向链表,光标上移或下移很方便)

image.png 双向链表的特点:
1、可以使用一个head和一个tail分别指向头部和尾部节点。
2、每个节点都有3部分组成:保存数据的item、指向前一个节点的指针pre、指向下一个节点的指针next。
3、双向链表的第一个节点的pre为null。
4、双向链表的最后一个节点的next是null。

  // 封装双向链表
  function DoublyLinkedlist() {
    // 内部类:节点类
    function Node(data) {
      this.data = data;
      this.prev = null;
      this.next = null;
    }
    // 属性
    this.head = null;
    this.tail = null;
    this.length = 0;
    // 常见的操作:方法
    // 向链表尾部添加新项
    DoublyLinkedlist.prototype.append = function(data){
      // 1.创建新节点
      var newNode = new Node(data);
      // 2.判断是否添加的是第一个节点
      if(this.length === 0) { // 2.1 第一个节点
        this.head = newNode;
        this.tail = newNode;
      } else {                // 2.2 不是第一个节点
        newNode.prev = this.tail;
        this.tail.next = newNode;
        this.tail = newNode;
      }
      // 3.长度+1
      this.length += 1;
    }
    // 向指定位置插入新项
    DoublyLinkedlist.prototype.insert = function(position, data){
      // 1.对position进行越界判断
      if(position < 0 || position > this.length) return false;
      // 2.创建新节点
      var newNode = new Node(data);
      // 3.判断列表是否为空
      if(this.length === 0) {
        this.head = newNode;
        this.tail = newNode;
      } else {
        if(position === 0) { // 3.1 判断position是否为0
          this.head.prev = newNode;
          newNode.next = this.head;
          this.head = newNode;
        } else if(position === this.length) { // 3.2 判断position是否为this.length(往最后插入)
          newNode.prev = this.tail;
          this.tail.next = newNode;
          this.tail = newNode;
        } else { // 3.3其它情况
          // 找到position对应的节点
          var index = 0;
          var current = this.head;
          while(index++ < position) {
            current = current.next;
          }
          // 更改指向
          newNode.next = current;
          newNode.prev = current.prev;
          current.prev.next = newNode;
          current.prev = newNode;
        }
      }
      // 长度+1
      this.length += 1;
      return true;
    }
    // 获取对应位置的元素
    DoublyLinkedlist.prototype.get = function(position){
      // 1.越界判断
      if(position < 0 || position >= this.length) return null;
      // 2.获取对应的data
      var current = this.head;
      var index = 0;
      while(index++ < position) {
        current = current.next;
      }
      return current.data;
    }
    // 返回元素在链表中的索引,没有则返回-1
    DoublyLinkedlist.prototype.indexOf = function(data) {
      // 1.定义变量
      var current = this.head;
      var index = 0;
      // 2.开始查找
      while(current) {
        if(current.data === data) {
          return index;
        }
        index++;
        current = current.next;
      }
      // 3.找到最后没有找到,返回-1
      return -1;
    }
    // 修改某个位置的元素
    DoublyLinkedlist.prototype.update = function(position,newData){
      // 1.越界判断
      if(position < 0 || position >= this.length) return false;
      // 2.寻找正确的节点
      var index = 0;
      var current = this.head;
      while(index++ < position) {
        current = current.next;
      }
      // 3.修改找到节点的data值
      current.data = newData;
      return true;
    }
    // 从链表中的特定位置移除一项
    DoublyLinkedlist.prototype.removeAt = function(position){
      // 1.越界判断
      if(position < 0 || position >= this.length) return null;
      // 2.判断是否只有一个节点
      var current = this.head;
      if(this.length === 1) {
        this.head = null;
        this.tail = null;
      } else {
        if(position === 0) { // 判断是否删除的是第一个节点
          this.head.next.prev = null;
          this.head = this.head.next;
        } else if(position === this.length -1) { // 如果是最后的节点
          var current = this.tail;
          this.tail.prev.next = null;
          this.tail = this.tail.prev;
        } else {
          // 找到对应位置的节点
          var index = 0;
          while(index++ < position) {
            current = current.next;
          }
          // 更改指针
          current.prev.next = current.next;
          current.next.prev = current.prev;
        }
        // 3.长度-1
        this.length -= 1;
        return current.data;
      }
    }
    // 从链表中移除一项
    DoublyLinkedlist.prototype.remove = function(data){
      // 1.根据data获取下标志
      var index = this.indexOf(data);
      // 2.根据index删除对应位置的节点
      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 ? this.head.data : null;
    }
    // 获取链表最后一个元素
    DoublyLinkedlist.prototype.getTail = function(){
      return this.tail ? this.tail.data : null;
    }
    // 以字符串形式输出链表
    DoublyLinkedlist.prototype.toString = function(){
      return this.backwardString();
    }
    // 返回向前遍历的节点字符串形式
    DoublyLinkedlist.prototype.forwardString = function(){
      // 1.定义变量
      var current = this.tail;
      var resultString = '';
      // 2.依次向前遍历,获得每一个节点
      while(current) {
        resultString += current.data + ' ';
        current = current.prev;
      }
      return resultString;
    }
    // 返回向后遍历的节点字符串形式
    DoublyLinkedlist.prototype.backwardString = function(){
      // 1.定义变量
      var current = this.head;
      var resultString = '';
      // 2.依次向后遍历,获得每一个节点
      while(current) {
        resultString += current.data + ' ';
        current = current.next;
      }
      return resultString;
    }

  }
  // 测试代码
  // 1.创建链表
  var list = new DoublyLinkedlist();
  // 2.测试追加方法
  list.append(1);
  list.append(2);
  list.append(3);
  console.log(list.toString());
  // 测试insert方法
  list.insert(0, 'tom');
  list.insert(2, 'bob');
  list.insert(list.length, 'july');
  // 测试get放放风
  console.log(list.get(0))
  console.log(list.get(1))
  console.log(list.get(9))
  // 测试indexOf方法
  console.log(list.indexOf('tom'))
  console.log(list.indexOf('july'))
  console.log(list.indexOf('aa'))
  // 测试update方法
  list.update(0, 'tom-update')
  list.update(5, 'july-update')
  // 测试removeAt方法
  // list.removeAt(0);
  // list.removeAt(2);
  // list.removeAt(3);
  // 测试remove方法
  // list.remove(3)
  // list.remove(8)
  // 其它方法测试
  console.log('isEmpty', list.isEmpty())
  console.log('size', list.size())
  console.log('getHead', list.getHead())
  console.log('getTail', list.getTail())
  console.log(list)
  // 测试toString方法
  console.log(list.forwardString());
  console.log(list.backwardString());

集合

集合比较常见的实现方式是(哈希表).
集合通常由一些无序的不能重复的元素构成。

  • 和数学中集合名词比较相似,但是数学中的集合范围更大一些,也允许集合中的元素重复。
  • 在计算机中,集合结构中的元素通常是不允许重复的。

特殊的数组

  • 特殊之处在于没有顺序也不能重复。
  • 没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中只能存在一份。

学习集合

  • 其实在2011年6月份发布的ES5中已经包含了Array类
  • 在2015年6月份发布的ES6中包含了 Set 类,我们可以不封装直接使用它。

简单封装一个集合类,了解集合的机制。

// 封装集合类
  function Set() {
    // 属性
    this.items = {};
    // 方法
    // 向集合添加一个新的项
    Set.prototype.add = function(value){
      // 判断集合中是否已经包含了该元素
      if(this.has(value)) return false;
      // 将元素添加到集合中
      this.items[value] = value;
      return true;
    }
    // 从集合移除一个值
    Set.prototype.remove = function(value){
      // 判断集合中是否已经包含了该元素
      if(!this.has(value)) return false;
      // 将元素从集合中删除
      delete this.items[value];
      return true;
    }
    // 判断集合中是否有对应的值
    Set.prototype.has = function(value){
      return this.items.hasOwnProperty(value);
    }
    // 移除集合中的所有项
    Set.prototype.clear = function(value){
      this.items = {};
    }
    // 返回集合所包含元素的数量,类似数组的length
    Set.prototype.size = function(value){
      return Object.keys(this.items).length;
    }
    // 返回集合汇总所有值的数组
    Set.prototype.values = function(value){
      return Object.values(this.items);
    }
  }
  // 测试
  var set = new Set();
  // 测试添加
  set.add(1)
  set.add(1)
  set.add(2)
  set.add(3)
  // 删除元素
  set.remove(1)
  // 测试has方法
  console.log(set.has(1))
  console.log(set.has(2))
  // 测试size方法
  console.log(set.size())
  // 测试clear方法
  set.clear()
  console.log(set.size())
  console.log(set)

集合的常见操作

集合的并集:返回一个包含两个集合所有元素的集合。
集合的交集:返回两个集合公有元素的新集合。
集合的差集:返回所有存在与第一个集合并且不存在于第二个集合的新集合。
子集:验证一个集合是否是另一个集合的子集。

image.png 实现集合的 并集、交集、差集、子集方法

// 封装集合类
  function Set() {
    // 属性
    this.items = {};
    // 方法
    // 向集合添加一个新的项
    Set.prototype.add = function(value){
      // 判断集合中是否已经包含了该元素
      if(this.has(value)) return false;
      // 将元素添加到集合中
      this.items[value] = value;
      return true;
    }
    // 从集合移除一个值
    Set.prototype.remove = function(value){
      // 判断集合中是否已经包含了该元素
      if(!this.has(value)) return false;
      // 将元素从集合中删除
      delete this.items[value];
      return true;
    }
    // 判断集合中是否有对应的值
    Set.prototype.has = function(value){
      return this.items.hasOwnProperty(value);
    }
    // 移除集合中的所有项
    Set.prototype.clear = function(value){
      this.items = {};
    }
    // 返回集合所包含元素的数量,类似数组的length
    Set.prototype.size = function(value){
      return Object.keys(this.items).length;
    }
    // 返回集合汇总所有值的数组
    Set.prototype.values = function(value){
      return Object.values(this.items);
    }
    // 集合间的操作
    // 并集  A U B
    Set.prototype.union = function(otherSet) {
      // this: 集合对象A
      // otherSet: 集合对象B
      // 1.创建一个新集合
      var unionSet = new Set();
      // 2.将A集合多有元素添加到新集合中
      var values = this.values();
      for(var i = 0; i < values.length; i++) {
        unionSet.add(values[i]);
      }
      // 3.取出B集合中的元素,判断是否需要加到新集合
      values = otherSet.values();
      for(var i = 0; i < values.length; i++) {
        unionSet.add(values[i]);
      }
      return unionSet;
    }
    // 交集 A ∩ B
    Set.prototype.intersection = function(otherSet) {
      // this: 集合对象A
      // otherSet: 集合对象B
      // 1.创建一个集合
      var intersectionSet = new Set();
      // 2.从A中取出一个个元素,判断是否同时存在与B集合,如果存在放入新的集合
      var values = this.values();
      for(var i = 0; i < values.length; i++) {
        var item = values[i];
        if(otherSet.has(item)) {
          intersectionSet.add(item)
        }
      }
      return intersectionSet;
    }
    // 差集 A - B
    Set.prototype.difference = function(otherSet) {
      // this: 集合对象A
      // otherSet: 集合对象B
      // 1.创建一个集合
      var differenceSet = new Set();
      // 2.从A中取出一个个元素,判断是否不存在B集合,如果不存在B集合,则添加到新集合
      var values = this.values();
      for(var i = 0; i < values.length; i++) {
        var item = values[i];
        if(!otherSet.has(item)) {
          differenceSet.add(item)
        }
      }
      return differenceSet;
    }
    // 子集 A 是否是 B的子集
    Set.prototype.subset = function(otherSet) {
      // this: 集合对象A
      // otherSet: 集合对象B
      // 遍历集合A中所有元素,如果发现,集合A中的元素在集合B中不存在,返回false
      // 如果遍历结束都没有返回false,返回true即可
      var values = this.values();
      for( var i = 0; i < values.length; i++) {
        if(!otherSet.has(values[i])) return false;
      }
      return true;
    }
  }
  // 并集测试
  // 1.创建两个集合,并且添加元素
  var setA = new Set();
  setA.add(1);
  setA.add(2);
  setA.add(3);
  var setB = new Set();
  setB.add(1);
  setB.add(2);
  setB.add(4);
  // 2.求两个集合的并集
  var unionSet = setA.union(setB);
  console.log(unionSet.values())
  // 3.求两个集合的交集
  var intersection = setA.intersection(setB);
  console.log('intersection', intersection.values())
  // 4.求两个集合的差集
  var differenceSet = setA.difference(setB);
  console.log('differenceSet', differenceSet.values())
  // 5.判断子集
  console.log('setA是否是setB的子集', setA.subset(setB))

字典

字典的特点

* 字典的主要特点是**一一对应的关系**
* 比如保存一个人的信息,在合适的情况下取出这些信息
* 使用数组的方式: [18, 'coder', 1.88]。可以通过下表取出信息。
* 使用字典的方式: { age: 18, name: 'coder', height: 1.88 }.可以通过key取出value
* 另外字典中的key是不可以重复的,而value可以重复,并且字典中的key是无需的。

* 字典和映射的关系,在有些变成语言中称这种映射关系为字典,还有些变成语言称这种映射关系为Map。

* 字典和数组对比,字典可以非常方便的通过key来搜索value,key更语义化。
* 字典和对象: 很多变成语言(比如java),对字典和对象区分比较明显,对象通常是在编译期就确定下来的结构,不可以动态添加或者删除属性,而字典通常会使用类似于哈希表的数据结构去实现一种可以动态添加数据的结构。
* 但是在javascript中,似乎对象本身就是一种字典,所有在早起的javascript中,没有字典这种类型,因为你完全可以使用对象去代替。

哈希表

* 哈希表是一种非常重要的数据结构,几乎所有的语言都直接或间接用到了这种结构
* 哈希表通常是有数组进行实现的,但是相对于数组,它有很多的优势
*** 它可以提供非常快速的 插入-删除-查找操作
*** 无论多少数据,插入和删除值需要接近常量的时间:即O(1)的时间级,实际上只需要几个机器指令即可完成。
*** 哈希表的速度比数还要快,几乎可以瞬间查找到想要的元素。
*** 哈希表相对于树来说编码要容易很多

*数组的一些优劣势:
***  数组进行插入操作时效率比较低(前面插入元素,后面的所有元素的位置需要变动)
*** 数组进行查找操作的效率
1)如果是记忆索引进行查找操作,效率非常高
2)基于内容去查找效率非常低(需要挨个去对比)
3)数组进行删除操作,效率也不高(后面的元素需要进行位移操作)

为了把数组操作效率低的为题解决,就有一些伟大的人发明了哈希表,使得数组比较耗时的操作效率变的非常快!!!

* 哈希表对于数组的一些不足
*** 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历期中的元素。
*** 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。

* 哈希表到底是什么?
*** 他的结构就是数组,但是他神奇的地方在于**对下标值的一种变换**,这种变换我们可以称之为哈希函数,通过哈希函数可以获取到HashCode

字母转数字

计算机中有许多编码方案,就是用数字代替单词的字符,就是字符编码
* 比如ASCII编码,a是97,b是98,依次类推122代表z。
* 我么你也可以设计一个自己的编码系统,比如a是1,b是2,c是3,依次类推,z是26
* 当然我们可以加上空格用0 代替,就是27个字符(不考虑大写问题)
* 但是有了编码系统后一个单词如何转成数字呢?
字符编码:
ASCII:美国使用的比较多,ASCII只用了128个字符,但是一些西方国家发现ASCII码不能满足他的需求,就在ASCII码基础上把剩余的128个扩充,满足了西方国家的编码,就形成了ISO-8859-1,但是这两种编码都不能满足中文,因为中文非常多,为了适用中文,发明了GBXXXX编码(GB2312(包含了常用的文字)->GBK(包含了更多专业的文字)->GB18030(包含了10万多的文字)),在很长的一段时间内,各个地方使用各自的编码,导致不能互相访问,后面出现了伟大的创想,unicode编码,把所有的字符集集于一身,他有很多的版本:UTF-32,UTF-16,UTF-8,目前用的非常多的就是utf-8编码。

数字相加方案:将字符对应的数字相加,但是这个缺点非常明显,不同的字母组合,相加的数字很大可能相等。

幂的连乘:比如7654=7*10³+6*10²+5*10¹+4*10º,这样基本保证单子获取到的数字的唯一性。但是有一个很大的缺点是,对于一些长单词,产生的下表会非常大,一个数组会占用很大的内存空间,而且有很多下标没存储东西,造成空间浪费。


对幂的连乘方案进行优化,让其得到的数字没那么大,如果有50000个单词,可能会定义一个长度为50000的数组,但实际情况往往需要更大的空间,因为我们不能保证单词会映射到每一个位置。比如给两倍的大小:100000,那么怎么把7000000000000这么大的数字压缩到100000以内呢?
取余操作:比如把0-199的数字压缩到0-913%10=3  157%10=7
但是这种也存在重复,不过可以继续优化。

哈希表的一些概念:
哈希化:将大数字转化成数组范围内下表的过程,我们就称之为哈希化。

哈希函数:通常我们会将单词转化成大数字,大数字再进行哈希化,我们称这样的函数叫哈希函数。

哈希表:最终将数据插入到这个数组,对整个结构的封装,我们就称之为哈希表。

思考:上面的哈希化还是有可能存在重复的可能,怎么去解决呢?

链地址法

image.png 我们在大数组的每个位置不是直接存值,而是存一个链表或数组,如果有重复的,就添加到对应位置的数组或链表内。

开放地址法

image.png

image.png

image.png

image.png

开放地址法和链地址法效率对比

image.png

image.png

image.png 二次探测和再哈希化比线性探测的效率要好一些。不过随着填充的数据越来越多,探测因子越来越大,两种方法的效率都会降低。
链地址法的效率降低降低的比较平缓,不会向二次探测和再哈希化的降低一样,到后期会成指数的方式降低。
所以在开发的时候使用链地址法的会比较多,开放地址法使用的会比较少。

优秀的哈希函数

哈希函数要简单,哈希表的初衷是速度快,如果在设计哈希函数时计算很复杂,导致比较耗时,就得不偿失了。
尽量的减少乘法和除法,因为计算机在做乘法或除法时比较消耗性能,尽量用其它方式替换乘法或除法。
尽量让元素在哈希表中均匀分布

image.png 用霍纳法则减少多项式的乘法:

image.png 在用时常量的地方尽量使用质数:
质数的概念:只能被1和他自身整除。

image.png 哈希表的长度为什么最好使用质数:
这涉及到数学中数论的东西,了解一下即可。
质数更有利于让数据在哈希表中均匀分布。

image.png

封装hash函数

// 设计哈希函数
  // 1、将字符串转换成比较大的数字:hashCode
  // 2、将hashCode压缩到数组范围之内
  function hashFunc(str, size) {
    // 1.定义hashCode变量
    var hashCode = 0;
    // 2.用霍纳算法来计算hashCode的值
    // cats => Unicode编码
    for(var i = 0; i < str.length; i++) {
      // 获取当前某一个字符的Unicode编码;
      // str.charCodeAt(i);
      // 这里的质数使用37,使用37的比较多
      hashCode = 37 * hashCode + str.charCodeAt(i);
    }
    // 3.取余操作
    var index = hashCode % size;
    return index;
  }

  // 测试哈希函数
  console.log(hashFunc('abc', 7)); // 4
  console.log(hashFunc('cba', 7)); // 3
  console.log(hashFunc('nba', 7)); // 5
  console.log(hashFunc('mba', 7)); // 1

封装哈希表

image.png 哈希表的添加或修改逻辑: image.png 哈希表的查找方法逻辑: image.png 哈希表的扩容思想: image.png 容量质数:

image.png 更高效率的判断质数:

image.png 代码封装

    // 哈希表封装(使用链地址法)
    function HashTable() {
      // 属性
      this.storage = []; // 存放数据的数组
      this.count = 0; // 当前哈希表存放的数据个数
      // loadFactor > 0.75,当装载因子或加载因子大于0.75时,效率会比较低,需要进行扩容;
      // 当loadFactor < 0.25时,如果数组表大,存的数据较少,还需要进行缩容;来提高效率;假如表中存放了5个数据,但是却用了一个长度为10000的数组,肯定不合理,需要缩容到5;
      this.limit = 7; // 用于记录数组的长度
      // 方法
      // 1、哈希函数
      HashTable.prototype.hashFunc = function(str, size) {
        // 1.定义hashCode变量
        var hashCode = 0;
        // 2.用霍纳算法来计算hashCode的值
        // cats => Unicode编码
        for(var i = 0; i < str.length; i++) {
          // 获取当前某一个字符的Unicode编码;
          // str.charCodeAt(i);
          // 这里的质数使用37,使用37的比较多
          hashCode = 37 * hashCode + str.charCodeAt(i);
        }
        // 3.取余操作
        var index = hashCode % size;
        return index;
      }
      // 2、插入和修改操作
      HashTable.prototype.put = function(key, value) {
        // 1.根据key获取index
        var index = this.hashFunc(key, this.limit);
        // 2.根据index取出对应的buket(桶)
        var buket = this.storage[index];
        // 3.判断该buket是否为null
        if(buket === undefined) {
          buket = [];
          this.storage[index] = buket;
        }
        // 4.判断是否是修改数据
        for(var i = 0; i < buket.length; i++) {
          // 元组(这里是个长度为2的数组[k,v]),代表桶内的每一个数组;
          var tuple = buket[i];
          if(tuple[0] === key) {
            tuple[1] = value;
            return;
          }
        }
        // 5.进行添加操作
        buket.push([key, value]);
        // 6.修改count值
        this.count += 1;
        // 7.扩容操作
        if(this.count > this.limit * 0.75) {
          var newLimit = this.getPrime(this.limit * 2);
          this.resize(newLimit);
        }
      }
      // 3、获取操作
      HashTable.prototype.get = function(key) {
        // 1.根据key获取index
        var index = this.hashFunc(key, this.limit);
        // 2.根据index取出对应的buket(桶)
        var buket = this.storage[index];
        // 3.判断该buket是否为null
        if(buket === undefined) {
          return null;
        }
        // 4.有buket,就进行线性查找
        for(var i = 0; i < buket.length; i++) {
          var tuple = buket[i];
          if(tuple[0] === key) {
            return tuple[1];
          }
        }
        // 5.依然没有找到,返回null
        return null;
      }
      // 4.删除操作
      HashTable.prototype.remove = function(key) {
        // 1.根据key获取index
        var index = this.hashFunc(key, this.limit);
        // 2.根据index取出对应的buket(桶)
        var buket = this.storage[index];
        // 3.判断该buket是否为null
        if(buket === null) {
          return null;
        }
        // 4.有buket,就进行线性查找,并且删除
        for(var i = 0; i < buket.length; i++) {
          var tuple = buket[i];
          if(tuple[0] === key) {
            tuple.splice(i, 1);
            this.count--;
            // 缩小容量
            if(this.limit > 7 && this.count < this.limit * 0.25) {
              var newLimit = this.getPrime(Math.floor(this.limit / 2));
              this.resize(newLimit);
            }
            return tuple[1];
          }
        }
        // 5.依然没有找到,返回null
        return null;
      }
      // 5、判断哈希表是否为空
      HashTable.prototype.isEmpty = function() {
        return this.count === 0;
      }
      // 6、获取哈希表中元素的个数
      HashTable.prototype.size = function() {
        return this.count;
      }
      // 7、哈希表扩容方法
      HashTable.prototype.resize = function(newLimit) {
        // 1.保存旧的数组内容
        var oldStorage = this.storage;
        // 2.重置所有的属性
        this.storage = [];
        this.count = 0;
        this.limit = newLimit;
        // 3.遍历oldStorage中所有的buket
        for(var i = 0; i< oldStorage.length; i++) {
          // 3.1取出对应的buket
          var buket = oldStorage[i];
          // 3.2判断buket是否为null
          if(buket === undefined) {
            continue;
          }
          // 3.3.buket中有数据,重新插入
          for(var j = 0; j < buket.length; j++) {
            var tuple = buket[j];
            this.put(tuple[0], tuple[1]);
          }
        }

      }
      // 8、判断是质数
      // 质数的特点:在大于1的自然数中,只能被1或自身整除的数,称为质数;
      // 不能被1到自身整除;
      // 如果一个数能被因数分解,那可两个因数一定是一个<=它的开平方根,另一个>它的开平方根;
      HashTable.prototype.isPrime = function (num) {
        if(num < 2 || !Number.isInteger(num)) return false;
        var temp = parseInt(Math.sqrt(num));
        for(var i = 2; i <= temp; i++) {
          if(num % i === 0) {
            return false;
          }
        }
        return true;
      }
      // 9、获取质数的方法
      HashTable.prototype.getPrime = function(num) {
        // 14 -> 17
        // 34 -> 37
        while(!this.isPrime(num)) {
          num ++;
        }
        return num;
      }
    }

    // 测试哈希表
    var ht = new HashTable();
    // 插入/修改数据
    ht.put('abc', 123);
    ht.put('cba', 321);
    ht.put('nba', 521);
    ht.put('mba', 520);
    ht.put('mba1', 522);
    ht.put('mba2', 523);
    ht.put('mba3', 524);
    ht.put('mba4', 525);
    // 修改
    ht.put('abc', 231);
    // 获取
    console.log('abc', ht.get('abc'));
    console.log('cba', ht.get('cba'));
    console.log('aaa', ht.get('aaa'));
    // 删除方法
    ht.remove('abc');
    console.log('abc', ht.get('abc'))
    // isEmpty、size
    console.log('isEmpty', ht.isEmpty());
    console.log('size', ht.size());
    ht.remove('mba1');
    ht.remove('mba2');
    ht.remove('mba3');
    ht.remove('mba4');

树结构

树结构的优点:数据结构的新增和删除比数组的性能消耗低,按元素查找时比数组快,但没有哈希表快,哈希表的空间利用率低,树结构的不存在这种问题,另外,树结构可以表示一对多的关系,这是一大特点。
树的术语

  • 树(Tree)是有n(n>=0)个节点构成的有限集合
  • 当n=0时,称之为空树
  • 对于一个非空树,它具备以下性质:
  • 树中有一个成为 (root)的特殊节点,用r表示。
  • 其余节点可分为m(m > 0)个互不相交的有限集T1、T2、T3、...Tm,其中每个集合本身又是一颗树,成为原来树的子树(subTree).
  • 节点的度(Degree):节点的子树个数。
  • 树的度:树的所有节点中最大的度数。
  • 叶节点(leaf):度为0的节点,也成为 叶子节点。
  • 父节点(parent):有子树的节点,是其子树根节点的父节点。
  • 子节点(child):若A节点时B节点的父节点,则B节点时A节点的子节点,子节点也称孩子节点。
  • 兄弟节点(sibling):具有同一父节点的子节点,彼此是兄弟节点。
  • 路径和路径长度:从节点n1到nk的路径为一个节点序列n1、n2...nk,ni是n(i+1)的父节点,路径所包含边的个数为路径长度。
  • 节点的层次(level):规定根节点在1层,其它任一节点的层数是是其父节点的层数加1.
  • 数的深度(depth):树中所有节点中最大层次,是这颗树的深度。

二叉树的概念

如果树中每个子节点最多只有两个子节点,这样的树就称为二叉树
任何树,最终都可以用二叉树模拟出来。(把其它树旋转一下,看着就是二叉树的样子)

二叉树的特性

  • 一个二叉树第i层的最大节点数为:2^(i-1), i >= 1;
  • 深度为k的二叉树,最大节点数为:2^k -1, k >= 1;
  • 对任何非空二叉树T,若n0表示叶节点的个数、n2是度为2的非叶节点的个数,那么两者满足关系n0=n2+1;(叶节点:没有子节点的节点。度:节点的子节点个数)

完美二叉树

完美二叉树也称为满二叉树,在二叉树中,除了最下一层的叶节点外,每成节点都有两个子节点,就构成了满二叉树。

完全二叉树

除了二叉树最后一层外,其它各层的节点数,都达到最大个数,
且最后一层从左向右的叶节点连续存在,只缺右侧若干节点。
完美二叉树是特殊的完全二叉树。

image.png

二叉搜索树

二叉搜索树(BST, binary search tree)也称二叉排序树二叉查找树

  • 二叉树可以为空。
  • 如果不空,满足以下性质:
  • 非空左子树所有键值小于其根节点的键值。
  • 非空右子树的所有键值大于其根节点的键值。
  • 左右子树本身也都是二叉搜索树。
    二叉搜索树的特点
  • 二叉搜索树的特点就是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。
  • 这也是二叉搜索树搜索速度快的原因。

image.png
先序遍(先处理根节点,再查找左节点,最后查找右节点)
11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
中序遍历(先查找左节点再处理根节点,最后查找右节点)
3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
后序遍历(先查找左节点,再查找右节点,最后处理根节点)
3 6 5 8 10 9 7 12 14 13 18 25 20 15 11

二叉搜索树代码封装:

// 封装二叉搜索树
  function BinarySearchTree() {
    function Node(key) {
      this.key = key;
      this.left = null;
      this.right = null;
    }
    // 属性
    this.root = null;
    // 方法
    // 向树中插入一个新键(对外暴露的方法)
    BinarySearchTree.prototype.insert = function(key) {
      // 1.根据key创建节点
      var newNode = new Node(key);
      // 2.判断根节点是否有值
      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);
        }
      }
    }
    // 先序遍历(先处理根节点)
    BinarySearchTree.prototype.preOrderTraverse = function(handler) {
      this.preOrderTraverseNode(this.root, handler);
    }
    BinarySearchTree.prototype.preOrderTraverseNode = function(node, handler) {
      if(node !== null) {
        // 1.处理经过的节点
        // if(node.key) console.log('处理跟节点', node.key);
        handler(node.key);
        // 2.查找左子节点
        // if(node.left && node.left.key)console.log('左节点', node.left.key, '入栈');
        this.preOrderTraverseNode(node.left, handler);
        // if(node.left && node.left.key)console.log('左节点', node.left.key, '出栈');
        // 3.查找右子节点
        // if(node.right && node.right.key)console.log('右节点', node.right.key, '入栈');
        this.preOrderTraverseNode(node.right, handler);
        // if(node.right && node.right.key)console.log('右节点', node.right.key, '出栈');
      }
    }
    // 中序遍历(先访问左节点再处理根节点,最后访问右节点)
    BinarySearchTree.prototype.midOrderTraverse = function(handler) {
      this.midOrderTraverseNode(this.root, handler);
    }
    BinarySearchTree.prototype.midOrderTraverseNode = function(node, handler) {
      if(node !== null) {
        // 1.查找左子树中的节点
        this.midOrderTraverseNode(node.left, handler);
        // 2.处理节点
        handler(node.key);
        // 3.查找右子树中的节点
        this.midOrderTraverseNode(node.right, handler);
      }
    }   
    // 后续遍历(先访问左节点再访问右节点,最后处理根节点)
    BinarySearchTree.prototype.postOrderTraverse = function(handler) {
      this.postOrderTraverseNode(this.root, handler);
    }
    BinarySearchTree.prototype.postOrderTraverseNode = function(node, handler) {
      if(node !== null) {
        // 1.查找左子树节点
        this.postOrderTraverseNode(node.left, handler);
        // 2.查找右子树节点
        this.postOrderTraverseNode(node.right, handler);
        // 3.处理根节点
        handler(node.key);
      }
    }
    // 在树中查找一个键,如果节点存在则返回true,如果不存在则返回false
    BinarySearchTree.prototype.search = function(key) {
      // while循环搜索方法(递归都可以用while循环来实现)
      var node = this.root;
      while(node !== null) {
        if(node.key > key) { // 如果key小于节点的key,向左找
          node = node.left;
        } else if(node.key < key) { // 如果key大于节点的key,向右找
          node = node.right;
        } else { // 如果key等于节点的key,返回true
          return true;
        }
      }
      // 如果没找到,返回false
      return false;
      // 递归搜索方法
      // return this.searchNode(this.root, key);
    }
    BinarySearchTree.prototype.searchNode = function(node, key) {
      // 如果为空树 返回false
      if(node === null) return false;
      if(node.key < key) { // 如果大于节点的key,想右搜索
        return this.searchNode(node.right, key);
      } else if(node.key > key) { // 如果小于节点的key,想左搜索
        return this.searchNode(node.left, key);
      } else { // 如果等于,返回true
        return true;
      }
    }
    // 返回树中最小的值/键
    BinarySearchTree.prototype.min = function() {
      // 获取根节点
      var node = this.root;
      // 如果为空树,返回null
      if(node === null) return null;
      // 依次向左不断的查找,直到左节点为null
      while(node.left !== null) {
        node = node.left;
      }
      return node.key;
    }
    // 返回树中最大的值/键
    BinarySearchTree.prototype.max = function() {
      // 获取根节点
      var node = this.root;
      // 如果为空树,返回null
      if(node === null) return null;
      // 依次向右不断的查找,直到右节点为null
      while(node.right !== null) {
        node = node.right;
      }
      return node.key;
    }
    // 总树中移除某个键
    BinarySearchTree.prototype.remove = function(key) {
      // 如果是空树,返回false
      if(this.root === null) return false;
      // 1.先查找到节点,如果没找到,就不需要删除
      // 1.1.定义变量保存一些信息
      var current = this.root;
      var parent = null;
      var isLeftChild = true;
      // 1.2开始寻找删除的节点
      while(current.key !== key) {
        parent = current;
        if(key < current.key) {
          isLeftChild = true;
          current = current.left;
        } else {
          isLeftChild = false;
          current = current.right;
        }
        // 如果找到叶子节点还没找到,返回false
        if(current === null) return false;
      }
      // 2.找到要删除的节点
      // 2.1.如果是叶子节点,就删除
      if(current.left === null && current.right === null) {
        if(current === this.root) {
          this.root = null;
        } else if(isLeftChild) {
          parent.left = null;
        } else {
          parent.right = null;
        }
      }
      // 2.2.如果只有一个子节点
      else if(current.right === null) {
        if(current === this.root) { // 如果删除的是根节点
          this.root = current.left;
        } else if(isLeftChild) {
          parent.left = current.left;
        } else {
          parent.right = current.left;
        }
      } 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;
        }
      }
      // 2.3.如果有两个子节点
      else {
        // 1.获取后继节点
        var successor = this.getSuccessor(current);
        // 2.判断是否是根节点
        if(current === this.root) {
          this.root = successor;
        } else if(isLeftChild) {
          parent.left = successor;
        } else {
          parent.right = successor;
        }
        // 3.将删除节点的左子树 = current.left
        successor.left = current.left;
      }
    }
    // 查找后继节点
    BinarySearchTree.prototype.getSuccessor = function(delNode) {
      // 1.保存变量,保存找到的后继
      var successor = delNode;
      var current = delNode.right;
      var successorParent = delNode;
      // 2.循环查找
      while(current !== null) {
        successorParent = successor;
        successor = current;
        current = current.left;
      }
      // 3.判断寻找的后继节点是否直接就是delNode的right节点
      if(successor !== delNode.right) {
        successorParent.left = successor.right;
        successor.right = delNode.right
      }
      return successor;
    }
  }

  // 测试代码
  var bst = new BinarySearchTree();
  bst.insert(11);
  bst.insert(7);
  bst.insert(15);
  bst.insert(5);
  bst.insert(3);
  bst.insert(9);
  bst.insert(8);
  bst.insert(10);
  bst.insert(13);
  bst.insert(12);
  bst.insert(14);
  bst.insert(20);
  bst.insert(18);
  bst.insert(25);
  bst.insert(6);
  console.log(bst, 'bst')
  // 测试遍历
  // 1.测试先序遍历
  var resultStr = "";
  bst.preOrderTraverse(function(key) {
    resultStr += key + ' ';
  })
  console.log('先序遍历', resultStr)
  // 2、测试中序遍历
  resultStr = "";
  bst.midOrderTraverse(function(key) {
    resultStr += key + ' ';
  })
  console.log('中序遍历', resultStr)
  // 测试后序遍历
  resultStr = "";
  bst.postOrderTraverse(function(key) {
    resultStr += key + ' ';
  })
  console.log('后序遍历', resultStr)
  console.log('最大值', bst.max())
  console.log('最小值', bst.min())
  console.log('11是否存在', bst.search(11))
  console.log('19是否存在', bst.search(19))
  console.log('3是否存在', bst.search(3))
  // 测试删除代码
  bst.remove(9)
  bst.remove(7)
  bst.remove(11)
  var resultStr = "";
  bst.preOrderTraverse(function(key) {
    resultStr += key + ' ';
  })
  console.log('先序遍历', resultStr)

二叉搜索树的优点
可以快速找到给定关键字的数据项,并且可以快速的插入和删除数据。(O(logN),100万的数据只需要查20次就能查到。
二叉搜索树的缺点
如果插入的数据是有序的,就会导致二叉搜索树分布不均,深度非常的深,比较好的二叉搜索树的数据应该是左右均匀分布的,我们称左右分布不均匀的树为非平衡树,对于一颗平衡树,插入/删除等操作的效率是O(logN),对于一个非平衡二叉树,相当于一个链表,查找效率变成O(N)了。\

image.png

为了能以较快的时间O(logN)来操作一棵树,我们需要保证树总是平衡的。\

  • 至少大部分是平衡的,那么时间复杂度也是接近O(logN)的
  • 也就是说树中的每个节点,左边的子孙节点的个数,应该尽可能等于右边子孙节点的个数。
    常见的平衡树有哪些

AVL树

AVL树是最早的一个平衡树,它有些办法保持树的平衡(每个节点多存储了一个额外的数据), 因为AVL树是平衡的,所以时间复杂度是O(logN).但是每次插入/删除相对于红黑树效率都不高, 所以整体效率不如红黑树。

红黑树的规则

红黑树除了满足二叉搜索树的规则外,还添加了以下特性:

  • 1、节点是红色或黑色
  • 2、根节点是黑色
  • 3、每个叶子节点都是黑色的空节点(NIL节点)
  • 4、每个红色节点的两个子节点都是黑色(每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

image.png

红黑树的相对平衡

  • 根到叶子最长可能路径,不会超过最短可能路径两倍长
  • 结果就是这棵树基本是平衡的。
  • 虽然没有做到绝对的平衡,但是可以保证在最坏的情况下,依然是高效的。

为什么可以做到最长路径不超过最短路径的两倍呢?

  • 性质4决定了路径不能有两个相连的红色节点。
  • 最短的可能路径都是黑色节点。
  • 最长的可能路径是红色和黑色交替。
  • 性质5所有的路径都有相同数目的黑色节点。
  • 这就表明了没有任何路径能多于其它路径的两倍长。

红黑树变换

插入一个新节点时,有可能树不再平衡,可以通过3种方式的变换,让树保持平衡。

换色-左旋转-右旋转

换色

为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。

首先,需要知道,插入的新的节点通常都是红色节点

  • 因为在插入节点为红色的时候,有可能插入一次是不违反红黑树任何规则的
  • 插入黑色节点,必然会导致有一条路径上多了黑色节点,这是很难调整的。
  • 红色节点可能导致红红相连的情况,但是这种情况可以通过颜色调换和旋转来调整。

旋转

左旋转

  • 逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而父节点成为右孩子的左节点。

右旋转

  • 顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而父节点成为左孩子的右节点。

image.png

插入操作

  • 设要插入的节点为N,其父节点为P
  • 祖父节点为G,其父亲的兄弟节点为U(即P和U是同一节点的子节点)

情况1

  • 新节点N位于树的根上,没有父节点。
  • 这种情况下,我们直接将红色变换成黑色即可,满足性质2.

情况2

  • 新节点的父节点P是黑色
  • 性质4没有失效(新节点是红色的),性质5也没有任何问题。
  • 尽管新节点N有两个叶子节点NIL,但是新节点N是红色的,所以通过它的路径中黑色节点的个数依然相同,满足性质5.

情况3

  • P为红色,U也是红色(父红叔红祖黑)
  • 变成 -> 父黑,叔黑,祖红

image.png
操作方案:

  • 将P和U变换为黑色,并且将G变换为红色
  • 现在新节点N有一个黑色的父节点P,所以每条路径上黑色节点的数目没有改变。
  • 而从更高的路径上,必然都会经过G节点,所以哪些路径的黑色节点数目也是不变的,符合性质5.
    可能出现的问题:\
  • 但是N的祖父节点G的父节点也可能是红色,这就违反了性质3,可以递归的调整颜色。
  • 但是递归调整颜色到根节点,就需要进行旋转了。

情况4

  • N的叔叔U是黑色,但N是左孩子(父红叔黑祖黑,N是左儿子)。
  • 变换 -> 父黑祖红,以祖为轴进行右旋转

image.png
操作方案:

  • 对祖父节点G一次进行右旋转
  • 在旋转查收的树中,以前的父节点P是以前祖父节点G的父节点。
  • 交换以前父节点P和祖父节点G的颜色(P为黑色,G为红色,G原来一定是黑色)
  • B节点向右平移,成为G的左子节点。

情况5

  • N的叔叔U是黑色节点(父红叔黑祖黑),且N是右孩子。
  • -> 以P为轴左旋转(变成情况4),自己变成黑色,祖父变成红色,以G为轴右旋转。

image.png
操作方案:

  • 对P节点进行依次左旋转,形成情况4的结果
  • 对父节点G进行一次右旋转,并且改变颜色即可

红黑树示例: 依次插入10 、 9、 8 、7 、 6 、 5 、 4 、 3 、 2 、 1

image.png

红黑树代码封装

  // 封装栈类
  function Stack() {
    // 栈中的属性
    this.items = [];
    // this.push = function() {}  是在每个类实例添加一个方法,不共享,而在原型上添加方法是共享的,可节省内存
    // 栈的操作
    // 1、将元素压入栈
    Stack.prototype.push = function (element) {
      this.items.push(element);
    }
    // 2、取出栈顶元素
    Stack.prototype.pop = function () {
      return this.items.pop();
    }
    // 3、查看栈顶元素
    Stack.prototype.peek = function () {
      return this.items[this.items.length - 1];
    }
    // 4、查看栈是否为空
    Stack.prototype.isEmpty = function () {
      return this.items.length === 0;
    }
    // 5、返回栈元素个数
    Stack.prototype.size = function () {
      return this.items.length;
    }
    // 6、将栈结构的内容以字符串的形式返回
    Stack.prototype.toString = function () {
      return this.items.toString();
    }
  }
// 自定义红黑树节点 RedBalckTreeNode
class MyRedBalckTreeNode {
   constructor(key = null, value = null, left = null, right = null) {
      this.key = key;
      this.value = value;
      this.left = left;
      this.right = right;
      this.color = MyRedBlackTree.RED; // MyRedBlackTree.BLACK;
   }

   // @Override toString 2018-11-25-jwl
   toString() {
      return (
         this.key.toString() +
         '--->' +
         this.value.toString() +
         '--->' +
         (this.color ? '红色节点' : '绿色节点')
      );
   }
}

// 自定义红黑树 RedBlackTree
class MyRedBlackTree {
   constructor() {
      MyRedBlackTree.RED = true;
      MyRedBlackTree.BLACK = false;

      this.root = null;
      this.size = 0;
   }

   // 判断节点node的颜色
   isRed(node) {
      // 定义:空节点颜色为黑色
      if (!node) return MyRedBlackTree.BLACK;

      return node.color;
   }

   //   node                     x
   //  /   \     左旋转         /  \
   // T1   x   --------->   node   T3
   //     / \              /   \
   //    T2 T3            T1   T2
   leftRotate(node) {
      const x = node.right;

      // 左旋转过程
      node.right = x.left;
      x.left = node;

      // 染色过程
      x.color = node.color;
      node.color = MyRedBlackTree.RED;

      // 返回这个 x
      return x;
   }

   // 颜色翻转 当前节点变红 左右孩子变黑
   // 表示当前节点需要继续向上进行融合
   flipColors(node) {
      node.color = MyRedBlackTree.RED;
      node.left.color = MyRedBlackTree.BLACK;
      node.right.color = MyRedBlackTree.BLACK;
   }

   //     node                   x
   //    /   \     右旋转       /  \
   //   x    T2   ------->   y   node
   //  / \                       /  \
   // y  T1                     T1  T2
   rightRotate(node) {
      const x = node.left;

      // 右翻转过程
      node.left = x.right;
      x.right = node;

      // 染色过程
      x.color = node.color;
      node.color = MyRedBlackTree.RED;

      // 返回这个 x
      return x;
   }

   // 比较的功能
   compare(keyA, keyB) {
      if (keyA === null || keyB === null)
         throw new Error("key is error. key can't compare.");
      if (keyA > keyB) return 1;
      else if (keyA < keyB) return -1;
      else return 0;
   }

   // 根据key获取节点 -
   getNode(node, key) {
      // 先解决最基本的问题
      if (!node) return null;

      // 开始将复杂的问题 逐渐缩小规模
      // 从而求出小问题的解,最后构建出原问题的解
      switch (this.compare(node.key, key)) {
         case 1: // 向左找
            return this.getNode(node.left, key);
            break;
         case -1: // 向右找
            return this.getNode(node.right, key);
            break;
         case 0: // 找到了
            return node;
            break;
         default:
            throw new Error(
               'compare result is error. compare result : 0、 1、 -1 .'
            );
            break;
      }
   }

   // 添加操作 +
   add(key, value) {
      this.root = this.recursiveAdd(this.root, key, value);
      this.root.color = MyRedBlackTree.BLACK;
   }

   // 添加操作 递归算法 -
   recursiveAdd(node, key, value) {
      // 解决最简单的问题
      if (!node) {
         this.size++;
         return new MyRedBalckTreeNode(key, value);
      }

      // 将复杂的问题规模逐渐变小,
      // 从而求出小问题的解,从而构建出原问题的答案
      if (this.compare(node.key, key) > 0)
         node.left = this.recursiveAdd(node.left, key, value);
      else if (this.compare(node.key, key) < 0)
         node.right = this.recursiveAdd(node.right, key, value);
      else {
         node.value = value;
         return node;
      }

      // 红黑树性质的维护
      // 是否需要左旋转
      // 如果当前节点的右孩子是红色 并且 左孩子不是红色
      if (this.isRed(node.right) && !this.isRed(node.left))
         node = this.leftRotate(node);

      // 是否需要右旋转
      // 如果当前节点的左孩子是红色 并且 左孩子的左孩子也是红色
      if (this.isRed(node.left) && this.isRed(node.left.left))
         node = this.rightRotate(node);

      // 是否需要颜色的翻转
      // 当前节点的左孩子和右孩子全都是红色
      if (this.isRed(node.left) && this.isRed(node.right))
         this.flipColors(node);

      // 最后返回这个node
      return node;
   }

   // 删除操作 返回被删除的元素 +
   remove(key) {
      let node = this.getNode(this.root, key);
      if (!node) return null;

      this.root = this.recursiveRemove(this.root, key);
      return node.value;
   }

   // 删除操作 递归算法 +
   recursiveRemove(node, key) {
      // 解决最基本的问题
      if (!node) return null;

      if (this.compare(node.key, key) > 0) {
         node.left = this.recursiveRemove(node.left, key);
         return node;
      } else if (this.compare(node.key, key) < 0) {
         node.right = this.recursiveRemove(node.right, key);
         return node;
      } else {
         // 当前节点的key 与 待删除的key的那个节点相同
         // 有三种情况
         // 1. 当前节点没有左子树,那么只有让当前节点的右子树直接覆盖当前节点,就表示当前节点被删除了
         // 2. 当前节点没有右子树,那么只有让当前节点的左子树直接覆盖当前节点,就表示当前节点被删除了
         // 3. 当前节点左右子树都有, 那么又分两种情况,使用前驱删除法或者后继删除法
         //      1. 前驱删除法:使用当前节点的左子树上最大的那个节点覆盖当前节点
         //      2. 后继删除法:使用当前节点的右子树上最小的那个节点覆盖当前节点

         if (node.left === null) {
            let rightNode = node.right;
            node.right = null;
            this.size--;
            return rightNode;
         } else if (node.right === null) {
            let leftNode = node.left;
            node.left = null;
            this.size--;
            return leftNode;
         } else {
            let predecessor = this.maximum(node.left);
            node.left = this.removeMax(node.left);
            this.size++;

            // 开始嫁接 当前节点的左右子树
            predecessor.left = node.left;
            predecessor.right = node.right;

            // 将当前节点从根节点剔除
            node = node.left = node.right = null;
            this.size--;

            // 返回嫁接后的新节点
            return predecessor;
         }
      }
   }

   // 删除操作的两个辅助函数
   // 获取最大值、删除最大值
   // 以前驱的方式 来辅助删除操作的函数

   // 获取最大值
   maximum(node) {
      // 再也不能往右了,说明当前节点已经是最大的了
      if (!node.right) return node;

      // 将复杂的问题渐渐减小规模,从而求出小问题的解,最后用小问题的解构建出原问题的答案
      return this.maximum(node.right);
   }

   // 删除最大值
   removeMax(node) {
      // 解决最基本的问题
      if (!node.right) {
         let leftNode = node.left;
         node.left = null;
         this.size--;
         return leftNode;
      }

      // 开始化归
      node.right = this.removeMax(node.right);
      return node;
   }

   // 查询操作 返回查询到的元素 +
   get(key) {
      let node = this.getNode(this.root, key);
      if (!node) return null;
      return node.value;
   }

   // 修改操作 +
   set(key, value) {
      let node = this.getNode(this.root, key);
      if (!node) throw new Error(key + " doesn't exist.");

      node.value = value;
   }

   // 返回是否包含该key的元素的判断值  +
   contains(key) {
      return this.getNode(this.root, key) !== null;
   }

   // 返回映射中实际的元素个数 +
   getSize() {
      return this.size;
   }

   // 返回映射中是否为空的判断值  +
   isEmpty() {
      return this.size === 0;
   }

   // @Override toString() 2018-11-05-jwl
   toString() {
      let mapInfo = `MyBinarySearchTreeMap: size = ${this.size}, data = [ `;
      // document.body.innerHTML += `MyBinarySearchTreeMap: size = ${
      //    this.size
      // }, data = [ <br/><br/>`;

      // 以非递归的前序遍历 输出字符串
      let stack = new Stack();

      stack.push(this.root);

      if (this.root === null) stack.pop();

      while (!stack.isEmpty()) {
         let node = stack.pop();

         if (node.left !== null) stack.push(node.left);
         if (node.right !== null) stack.push(node.right);

         if (node.left === null && node.right === null) {
            mapInfo += ` ${node.toString()} \r\n`;
            // document.body.innerHTML += ` ${node.toString()} <br/><br/>`;
         } else {
            mapInfo += ` ${node.toString()}, \r\n`;
            // document.body.innerHTML += ` ${node.toString()}, <br/><br/>`;
         }
      }

      mapInfo += ` ] \r\n`;
      // document.body.innerHTML += ` ] <br/><br/>`;

      return mapInfo;
   }
}


// 测试代码
var arr = [10,9,8,7,6,5,4,3,2,1];
var rbt = new MyRedBlackTree();
// 添加操作
arr.forEach(i => {
  rbt.add(i, i);
})
console.log('rbt', rbt.toString())
// 删除key
rbt.remove(11);
// 更新操作
rbt.set(7, 10)
console.log('rbt', rbt)
// 查找
console.log('查找key为7的节点', rbt.get(7))
console.log('查找key为13的节点', rbt.get(13))

图论(Graph)

图通常有什么特点

  • 一组顶点:通常用V(Vertex)表示顶点的集合。
  • 一组边:通常用E(Edge)表示边的集合。
    边是顶点和顶点之间的连线。
    边可以是有向的也可以是无向的。
    比如A --- B 通常表示无向, A--> B通常表示有向。

图的术语

  • 顶点:图中的某一个节点
  • :顶点和顶点之间的连线
  • 相邻顶点:由一条边相连在一起的顶点成为相邻顶点
  • :一个顶点的度是相邻顶点的数量
  • 路径:顶点v1、v2...vn的连续序列
  • 简单路径:要求不包含重复的顶点
  • 回路:第一个顶点和最后一个顶点相同的路径成为回路
  • 无向图:所有的边都没有方向
  • 有向图:图中的边是有方向的
  • 无权图:边没有携带权重
  • 带权图:边有一定的权重

图的表示方法

  • 邻接矩阵

image.png 邻接矩阵的问题:

image.png

  • 邻接表
    有点像链表,每个顶点用一个数组存储和自己相连的顶点。

image.png

image.png

字典封装

// 创建字典构造函数
function Dictionary() {
  // 字典属性
  this.items = {};
  // 字典操作方法
  // 在字典中添加键值对
  Dictionary.prototype.set = function(key, value) {
    this.items[key] = value;
  }
  // 判断字典中是否有某个key
  Dictionary.prototype.has = function(key) {
    return this.items.hasOwnProperty(key);
  }
  // 从字典奕宏移除元素
  Dictionary.prototype.remove = function(key) {
    // 判断字典中是否有这个key
    if(!this.has(key)) return false;
    // 从字典中删除key
    delete this.items[key];
    return true;
  }
  // 根据key去获取value
  Dictionary.prototype.get = function(key) {
    return this.has(key) ? this.items[key] : undefined;
  }
  // 获取所有的keys
  Dictionary.prototype.keys = function() {
    return Object.keys(this.items);
  }
  // 获取所有的value
  Dictionary.prototype.values = function() {
    return Object.values(this.items);
  }
  // size方法
  Dictionary.prototype.size = function() {
    return this.keys().length;
  }
  // clear方法
  Dictionary.prototype.clear = function() {
    this.items = {};
  }
}

图的遍历

图的遍历思想:访问每一个顶点,并且不能重复的访问。
有两种算法可以对图进行遍历:

  • 广度优先搜索(Breadth-First Search,简称BFS),访问某个顶点的所有相邻顶点,再依次访问相邻顶点的相邻顶点。
  • 深度优先搜索(Depth-First Search,简称DFS),一黑到底,直接把某条线走完。然后依次回退,检查每个顶点是否有未访问的顶点,如果有就一黑到底的访问,然后再回退,再检查,循环处理。
  • 两种遍历算法,都需要明确指定第一个被访问的顶点

image.png
两种算法的思想

  • BFS:基于队列,入队列的顶点先被搜索
  • DFS:基于栈或使用递归,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问。

为了记录顶点是否被访问过,我们使用 三种颜色 来访问他们的状态

  • 白色:表示该顶点还没有被访问
  • 灰色:表示该顶点被访问过,但并未被搜索。
  • 黑色:表示该顶点被访问过且被完全探索过。

广度优先搜索思路:

image.png

深度优先搜索思路:

image.png

图结构封装

采用邻接表的方式

image.png

  // 封装图结构
  function Graph() {
    // 属性:顶点(数组)/边(字典)
    this.vertexes = []; // 顶点
    this.edges = new Dictionary();
    // 方法
    // 1.添加顶点的方法
    Graph.prototype.addVertex = function(v) {
      this.vertexes.push(v);
      this.edges.set(v, []);
    }
    // 2.添加边的方法
    Graph.prototype.addEdge = function(v1, v2) {
      this.edges.get(v1).push(v2);
      this.edges.get(v2).push(v1); // 无向图(两顶点互相连接)
    }
    // 3.toString方法
    Graph.prototype.toString = function() {
      // 定义字符串,保留最终结果
      var resultString = '';
      // 遍历所有顶点,以及顶点对应的边
      for(var i = 0; i < this.vertexes.length; i++) {
        resultString += this.vertexes[i] + '->';
        var vEdges = this.edges.get(this.vertexes[i]);
        for(var j = 0; j < vEdges.length; j++) {
          resultString += vEdges[j] + ' ';
        }
        resultString += '\n';
      }
      return resultString;
    }
    // 4.初始化状态颜色
    Graph.prototype.initializeColor = function() {
      var colors = {};
      for(var i = 0; i < this.vertexes.length; i++) {
        colors[this.vertexes[i]] = 'white';
      }
      return colors;
    }
    // 5.实现广度优先搜索(BFS)
    Graph.prototype.bfs = function(initV, handler) {
      // 1.初始化颜色
      var colors = this.initializeColor();
      // 2.创建队列
      var queue = new Queue();
      // 3.将初始顶点加入到队列中
      queue.enqueue(initV);
      // 4.循环从队列中取出元素
      while(!queue.isEmpty()) {
        // 4.1 从队列中取出一个顶点
        var v = queue.dequeue();
        // 4.2 获取和顶点相邻的边
        var vList = this.edges.get(v);
        // 4.3将v的颜色设置为灰色(探索过,但未访问)
        colors[v] = 'gray';
        // 4.4遍历所有的顶点并且加入到顶点中
        for(var i = 0; i < vList.length; i++) {
          var e = vList[i];
          if(colors[e] === 'white') { // 避免重复添加
            colors[e] = 'gray';
            queue.enqueue(e);
          }
        }
        // 4.5 访问顶点v
        handler(v);
        // 4.6 将v顶点设置为黑色(访问过)
        colors[v] = 'black';
      }
    }
    // 6.实现深度优先搜索(DFS)
    Graph.prototype.dfs = function(initV, handler) {
      // 1.初始化颜色
      var colors = this.initializeColor();
      // 2.从某个顶点开始依次递归访问
      this.dfsVisit(initV, colors, handler);
    }
    // 6.1 深度优先遍历的递归方法
    Graph.prototype.dfsVisit = function(v, colors, handler) {
      // 1.先将颜色设置为灰色
      colors[v] = 'gray';
      // 2.处理v顶点
      handler(v);
      // 3.访问v相连的其它顶点
      var vList = this.edges.get(v);
      for(var i = 0; i < vList.length; i++) {
        var e = vList[i];
        if(colors[e] === 'white') { // 防止重复访问
          this.dfsVisit(e, colors, handler);
        }
      }
      // 4.将v设置成黑色
      colors[v] = 'black';
    }
  }
  // 测试代码
  // 1.创建图结构
  var graph = new Graph();
  // 2.添加顶点
  var myVertexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
  myVertexes.forEach(v => {
    graph.addVertex(v);
  });
  // 3.添加边
  graph.addEdge('A', 'B');
  graph.addEdge('A', 'C');
  graph.addEdge('A', 'D');
  graph.addEdge('C', 'D');
  graph.addEdge('C', 'G');
  graph.addEdge('D', 'G');
  graph.addEdge('D', 'H');
  graph.addEdge('B', 'E');
  graph.addEdge('B', 'F');
  graph.addEdge('E', 'I');
  // 4.测试toString方法
  console.log(graph.toString())
  // 5.测试bfs
  var result = '';
  graph.bfs(graph.vertexes[0], function(v) {
    result += v + ' ';
  });
  console.log('bfs', result);
  // 6.测试dfs
  result = '';
  graph.dfs(graph.vertexes[0], function(v) {
    result += v + ' ';
  });
  console.log('dfs', result);

排序算法

认识大O表示法

image.png

常见的大O表示形式

image.png

希尔排序增量及时间复杂度

image.png

希尔排序思路

image.png

快速排序的思想(分而治之)
13为枢纽,left为左指针,right为右指针,每次left找到一个比枢纽大的值,right找到一个比13小的值, 然后left、right指针位置的值交换位置,然后继续让左指针向右找,有指针向左找,直到两个指针重合,最后让指针位置的值和枢纽值(13)交换,这一轮下去,13做边的值都比13小,13右边的值都比13大。
然后在左侧、和右侧进行同样的操作(选择一个枢纽,把小于他的放到左侧,把大于他的放到右侧)

image.png 最合适的枢纽选择方案:第一个值和随机数都不是最好我选择,第一个值的效率有些低,随机数本身就消耗性能,最合适的方案是选取索引0、length-1、(left+right)/2,然后取中位数放到中间,直接放在最大值的前面

image.png

image.png

image.png

排序代码实现

  // 创建列表类
  function ArrayList() {
    // 属性
    this.array = [];
    // 方法
    // 将数据插入数组中的方法
    ArrayList.prototype.insert = function(item) {
      this.array.push(item);
    }
    // toString方法
    ArrayList.prototype.toString = function(){
      return this.array.join('-');
    }
    // 交换两个位置的数据
    ArrayList.prototype.swap = function(m,n){
      var temp = this.array[m];
      this.array[m] = this.array[n];
      this.array[n] = temp;
    }
    // 实现排序算法
    // 冒泡排序
    // 比较次数 N*(N-1)/2   大O表示为 O(N²), 假如两次比较需要依次交换,这交换次数为N*(N-1)/4  大O表示为 O(N²)
    ArrayList.prototype.bubbleSort = function(){
      // 获取数组长度
      var length = this.array.length;
      // 第一次:j = length - 1;比较到倒数第一个位置
      // 第二次:j = length - 2;比较到倒数第二个位置
      // ...
      for(var j = length - 1; j >= 0; j--) {
        // 第一次进来 i=0,比较0和1位置的两个数据,如果0位置的数据大于1位置的数据,则交换位置
        // 最后一次进来: i = j-2, 比较 j-2和j -1 的两个数据;
        for(var i = 0; i < j; i++) {
          if(this.array[i] > this.array[i+1]) {
            // 交换位置
            this.swap(i, i+1);
          }
        }
      }
    }
    // 选择排序
    // 改进了冒泡排序,将交换次数由O(N²)变成了O(N),但是比较次数依然是O(N²)
    // 选择排序比较次数为N*(N-1)/2   大O表示为 O(N²),交换次数为N-1次,大O表示为 O(N)
    ArrayList.prototype.selectionSort = function(){
      // 获取数组长度
      var length = this.array.length;
      // 2.外层循环:锁钉要放置最小值的位置
      for(var j = 0; j < length - 1; j++) { // 最后一个就不用比较了,肯定是最大的
        // 内层循环:从j+1位置开始,和后面的数进行比较,找到最小值
        var min = j;
        for(var i = min + 1; i < length; i++) {
          if(this.array[min] > this.array[i]) {
            min = i;
          }
        }
        this.swap(min, j);
      }
    }
    // 插入排序
    // 插入排序的思路是把局部看做有序(是希尔排序、快速排序的基础)
    // 默认将第一个元素看做是有序的,用第二个元素和前面的元素比较,如果前面的元素大于第二个元素,则前面的元素后移,否则,插入到此位置
    // 插入排序最多比较次数为N*(N-1)/2 大O表示法为O(N²) 
    // 插入排序最多复制次数为N*(N-1)/2 大O表示法为O(N²) 
    ArrayList.prototype.insertionSort = function(){
      // 1.获取数组长度
      var length = this.array.length;
      // 2.外层循环:从第1个位置开始获取数据,向前面局部有序进行插入
      for(var i = 1; i < length; i++) {
        // 3.内层循环:获取i位置的元素,和前面的数据依次进行比较
        var temp = this.array[i];
        var j = i;
        while(this.array[j - 1] > temp && j > 0) {
          this.array[j] = this.array[j - 1];
          j--;
        }
        // 4.将temp数据放置到j位置
        this.array[j] = temp;
      }
    }
    // 希尔排序
    // 希尔排序是插入排序的改进版
    // 希尔排序是排序算法的一个里程碑,在之前的好长一段时间内,人们发现排序算法的时间复杂度不可能超过O(N²),
    // 终于有一天,一个名叫希尔的数学家发明了这个排序,让效率超过了O(N²),于是给这个排序算法命名为希尔排序
    // 希尔排序的思路:让小的数在每次比较后尽量靠前,希尔先定义一个间隔(gap),让间隔一样的数分成一组,这样每次做冒泡排序会让小的数尽量靠前
    // 逐步缩小间隔,知道间隔为1,进行一次冒泡排序,就会变成全部有序(希尔排序的优化触发点是减少比较和插入次数)
    // 最终证明,希尔排序在最坏的情况下是O(N²),一般情况下效率是高于O(N²)的。
    // 在比较好的增量下,有时候效率甚至好于快速排序
    ArrayList.prototype.shellSort = function(){
      // 1.获取数组长度
      var length = this.array.length;
      // 2.初始化的增量(gap -> 间隔/间隙)
      var gap = Math.floor(length / 2);
      // 3.while循环,让gap不断减小
      while(gap >= 1) {
        // while内部是冒泡排序
        // 4.以gap为间隔,进行分组,对分组进行插入排序
        for(var i = gap; i < length; i+= gap) {
          var temp = this.array[i];
          var j = i;
          while(this.array[j - gap] > temp && j > gap - 1) {
            this.array[j] = this.array[j - gap]; // 后移操作
            j -= gap;
          }
          // 5.将temp放到j位置
          this.array[j] = temp;
        }
        // 6.
        gap = Math.floor(gap / 2);
      }
    }
    // 快速排序
    // 快速排序可以说是所有排序中最快的一种排序算法
    // 虽然有时候,希尔排序会比快速排序好一些,但是一般情况下,快速排序是最好的选择
    // 快速排序可以在一次循环中(其实是递归调用),找出某个元素正确位置,并且该元素之后不需要任何移动
    // 快速排序的重要思想是分而治之

    // 快速排序-选择枢纽
    ArrayList.prototype.median = function(left, right){
      // 1.去出中间位置
      var center = Math.floor((left + right) / 2);
      // 2.判断大小,并且进行交换
      if(this.array[left] > this.array[center]) {
        this.swap(left, center);
      }
      if(this.array[center] > this.array[right]) {
        this.swap(center, right);
      }
      if(this.array[left] > this.array[center]) {
        this.swap(left, center);
      }
      // 3.将center换到right-1的位置
      this.swap(center, right - 1);
      // 4.返回枢纽
      return this.array[right - 1];
    }
    // 快速排序实现
    // 快速排序的平均效率是O(N*log(N))
    ArrayList.prototype.quickSort = function(){
      this.quick(0, this.array.length - 1);
    }
    // 快速排序-递归函数
    ArrayList.prototype.quick = function(left, right){
      // 1.结束条件
      // if(left >= right) return; // 这个结束条件未考虑到left和right相邻的情况,这样找到的枢纽和left重合,会出错
      if(right - left <= 1) return;
      // 2.找枢纽
      var pivot = this.median(left, right);
      // 3.定义变量,用于记录当前找到的位置
      var i = left;
      var j = right - 1;
      // 4.开始进行交换
      while(true) {
        while(this.array[++i] < pivot) {}
        while(this.array[--j] > pivot) {}
        if(i < j) {
          this.swap(i, j);
        } else {
          // i >= j
          break;
        }
      }
      // 5.将枢纽放置在正确的位置,i的位置
      this.swap(i, right - 1);
      // 6.分而治之
      this.quick(left, i - 1);
      this.quick(i + 1, right);
    }
  }
  // 测试代码
  var list = new ArrayList();
  // 插入数据
  list.insert(66);
  list.insert(88);
  list.insert(12);
  list.insert(56);
  list.insert(566);
  list.insert(321);
  console.log(list.toString())
  // 验证冒泡排序
  // list.bubbleSort();
  // console.log(list.toString())
  // 验证选择排序
  // list.selectionSort();
  // console.log(list.toString())
  // 验证插入排序
  // list.insertionSort();
  // console.log(list.toString()) 
  // 验证希尔排序
  // list.shellSort();
  // console.log(list.toString()) 
  // 验证快速排序
  list.quickSort();
  console.log(list.toString())