JS 数据结构(一)--栈,队列,链表

1,346 阅读13分钟

前言

​ 在javascript中,有几种数据结构,本章要向你讲解的是 队列链表

定义
  • 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的结构。

  • 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底

  • 在栈里,新元素都靠近栈顶,旧元素都接近栈底。

  • 从栈的操作特性来看,是一种 操作受限的线性表,只允许在一端插入和删除数据。

  • 不包含任何元素的栈称为空栈

现实场景

​ 如现实中的一摞书和餐厅里叠放的盘子,如下

使用场景

​ 被用在编程语言的编译器和内存中保存变量,方便调用等,还被用于浏览器历史记录

实现

栈的方法:

  • push(element):添加一个(或几个)新元素到栈顶。

  • pop():移除栈顶的元素,同时返回被移除的元素。

  • peek():返回栈顶的元素,不对栈做任何修改。

  • isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。

  • clear():移除栈里的所有元素。

  • size():返回栈里的元素个数。

    // Stack类
class Stack {
  constructor(){
    this.items = []
  }
 

  // 添加新元素到栈顶
  push (element) {
    this.items.push(element);
  }
  // 移除栈顶元素,同时返回被移除的元素
  pop() {
    return this.items.pop();
  }
  // 查看栈顶元素
  peek () {
    return this.items[this.items.length - 1];
  }
  // 判断是否为空栈
  isEmpty () {
    return this.items.length === 0;
  }
  // 清空栈
  clear () {
    this.items = [];
  }
  // 查询栈的长度
  size () {
    return this.items.length;
  }
  // 打印栈里的元素
  print () {
    console.log(this.items.toString());
  }
}



// 创建Stack实例
var stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(5); // undefined
stack.push(8); // undefined
console.log(stack.peek()); // 8
stack.push(11); // undefined
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
stack.push(15); // undefined
stack.pop(); // 15
console.log(stack.size()); // 3
stack.print(); // 5,8,11
stack.clear(); // undefined
console.log(stack.size()); // 0

扩展

把十进制转换成二进制 (要把十进制转换成二进制,可以将改十进制数除以2,二进制是满二进一,并对商取整,知道结果是0为止)

function decimalToBinary(decNumber){
  const remStack = new Stack();
  let number = decNumber;
  let rem;
  let binaryString = '';

  while (number>0) {
    rem = Math.floor(number % 2);
    remStack.push(rem)
    number = Math.floor(number / 2)
  }

  while (!remStack.isEmpty()) {
    binaryString += remStack.pop().toString() 
  }

  return binaryString

}

console.log(decimalToBinary(233))  //11101001

队列

普通队列

定义
  • 队列是遵循 FIFO(First In First Out,先进先出)原则的一组有序的项。
  • 队列在尾部添加新元素,并从顶部移除元素。
  • 最新添加的元素必须排在队列的末尾。
  • 队列只有 入队 push() 和出队 pop()。
现实场景

​ 商场排队

实现
  • enqueue(element):向队列尾部添加新项。

  • dequeue():移除队列的第一项,并返回被移除的元素。

  • front():返回队列中第一个元素,队列不做任何变动。

  • isEmpty():如果队列中不包含任何元素,返回 true,否则返回 false。

  • size():返回队列包含的元素个数,与数组的 length 属性类似。

  • print():打印队列中的元素。

  • clear():清空整个队列。

class Queue {
  constructor(){
    this.items = []
  }

  // 向队列尾部添加元素
  enqueue(element){
    this.items.push(element);
  }

  // 移除队列的第一个元素,并返回被移除的元素
  dequeue (){
    return this.items.shift();
  }

  // 返回队列的第一个元素
  front(){
    return items[0];
  }

  // 判断是否为空队列
  isEmpty (){
    return items.length == 0;
  }

  // 清空队列
  clear (){
    this.items = [];
  }

  // 获取队列的长度
  size (){
    return this.items.length;
  }

  // 打印队列里的元素
  print (){
    console.log(this.items.toString());
  }
}


// 创建Queue实例
var queue = new Queue();
console.log(queue.isEmpty()); // true
queue.enqueue('John'); // undefined
queue.enqueue('Jack'); // undefined
queue.enqueue('Camila'); // undefined
queue.print(); // "John,Jack,Camila"
console.log(queue.size()); // 3
console.log(queue.isEmpty()); // false
queue.dequeue(); // "John"
queue.dequeue(); // "Jack"
queue.print(); // "Camila"
queue.clear(); // undefined
console.log(queue.size()); // 0


循环队列

定义

循环队列,顾名思义,它长得像一个环。把它想像成一个圆的钟就对了。

关键是:确定好队空和队满的判定条件。

现实场景

鼓传花游戏(Hot Potato)。在这个游戏中,孩子们围城一个圆圈,击鼓的时候把花尽快的传递给旁边的人。某一时刻击鼓停止,这时花在谁的手里,谁就退出圆圈直到游戏结束。重复这个过程,直到只剩一个孩子(胜者)

实现
// 实现击鼓传花
function hotPotato (nameList, num) {
  var queue = new Queue();

  for (var i = 0; i < nameList.length; i++) {
    queue.enqueue(nameList[i]);
  }

  var eliminated = '';

  while (queue.size() > 1) {
    // 循环 num 次,队首出来去到队尾
    for (var i = 0; i < num; i++) {
      queue.enqueue(queue.dequeue());
    }
    // 循环 num 次过后,移除当前队首的元素
    eliminated = queue.dequeue();
    console.log(`${eliminated} 在击鼓传花中被淘汰!`);
  }

  // 最后只剩一个元素
  return queue.dequeue();
}

// 测试
var nameList = ["John", "Jack", "Camila", "Ingrid", "Carl"];
var winner = hotPotato(nameList, 10);
console.log(`最后的胜利者是:${winner}`);


//执行结果为:
// John 在击鼓传花中被淘汰!
// Ingrid 在击鼓传花中被淘汰! 
// Jack 在击鼓传花中被淘汰!
// Camila 在击鼓传花中被淘汰!
// 最后的胜利者是:Carl

链表

定义
  • 链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的,它是通过 指针零散的内存块 串连起来的。
  • 每个元素由一个存储元素本身的 节点 和一个指向下一个元素的 引用(也称指针或链接)组成。
特点
  • 链表是通过指针将零散的内存块串连起来的。 所以链表不支持 随机访问,如果要找特定的项,只能从头开始遍历,直到找到某个项。 所以访问的时间复杂度为 O(n)。

  • 高效的插入和删除。 链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的,只需要考虑相邻结点的指针改变。 所以,在链表中插入和删除一个数据是非常快速的,时间复杂度为 O(1)。

单链表

定义

由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。

针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以插入与删除的时间复杂度为 O(1)。

实现
  • Node 类用来表示节点。
  • LinkedList 类提供插入节点、删除节点等一些操作。

单向链表的八种常用操作:

  • append(element):尾部添加元素。
  • insert(position, element):特定位置插入一个新的项。
  • removeAt(position):特定位置移除一项。
  • remove(element):移除一项。
  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回 -1。
  • isEmpty():如果链表中不包含任何元素,返回 true,如果链表长度大于 0,返回 false。
  • size():返回链表包含的元素个数,与数组的 length 属性类似。
  • getHead():返回链表的第一个元素。
  • toString():由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值。
  • print():打印链表的所有元素。
 class Node {
  constructor(element) {
    this.element = element; // 当前节点的元素
    this.next = null; // 下一个节点指针
  }
}



// 单链表
class SinglyLinkedList {
  constructor() {
    this.length = 0; // 链表的长度
    this.head = null; // 链表的头部节点
  }


  // 向链表尾部添加一个新的节点
  append(element) {
    var node = new Node(element);
    var currentNode = this.head;

    // 判断是否为空链表
    if (this.head === null) {
      // 是空链表,就把当前节点作为头部节点
      this.head = node;
    } else {
      // 从 head 开始一直找到最后一个 node
      while (currentNode.next) {
        // 后面还有 node
        currentNode = currentNode.next;
      }
      // 把当前节点的 next 指针 指向 新的节点
      currentNode.next = node;
    }
    // 链表的长度加 1
    this.length++;
  }

  // 向链表特定位置插入一个新节点
  insert(position, element) {
    if (position < 0 && position > this.length) {
      // 越界
      return false;
    } else {
      var node = new Node(element);
      var index = 0;
      var currentNode = this.head;
      var previousNode;

      // 在最前插入节点
      if (position === 0) {
        node.next = currentNode;
        this.head = node;
      } else {
        // 循环找到位置
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        // 把前一个节点的指针指向新节点,新节点的指针指向当前节点,保持连接性
        previousNode.next = node;
        node.next = currentNode;
      }

      this.length++;

      return true;
    }
  };

  // 从链表的特定位置移除一项
  removeAt(position) {
    if ((position < 0 && position >= this.length) || this.length === 0) {
      // 越界
      return false;
    } else {
      var currentNode = this.head;
      var index = 0;
      var previousNode;

      if (position === 0) {
        head = currentNode.next;
      } else {
        // 循环找到位置
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        // 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
        previousNode.next = currentNode.next;
      }

      this.length--;

      return true;
    }
  };

  // 从链表中移除指定项
  remove(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  };

  // 返回元素在链表的索引,如果链表中没有该元素则返回 -1
  indexOf(element) {
    var currentNode = this.head;
    var index = 0;

    while (currentNode) {
      if (currentNode.element === element) {
        return index;
      }

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };

  // 如果链表中不包含任何元素,返回 true,如果链表长度大于 0,返回 false
  isEmpty() {
    return this.length === 0;
  };

  // 返回链表包含的元素个数,与数组的 length 属性类似
  size() {
    return this.length;
  };

  // 获取链表头部元素
  getHead = function () {
    return this.head.element;
  };

  // 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
  toString() {
    var currentNode = this.head;
    var string = '';

    while (currentNode) {
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  };

  // 打印链表数据
  print() {
    console.log(this.toString());
  };

  // 获取整个链表
  list() {
    console.log('head', this.head)
    return this.head;
  };
}

// 创建单向链表实例
var singlyLinked = new SinglyLinkedList();
console.log(singlyLinked.removeAt(0)); // false
console.log(singlyLinked.isEmpty()); // true
singlyLinked.append('Tom');
singlyLinked.append('Peter');
singlyLinked.append('Paul');
singlyLinked.print(); // "Tom,Peter,Paul"
singlyLinked.insert(0, 'Susan');
singlyLinked.print(); // "Susan,Tom,Peter,Paul"
singlyLinked.insert(1, 'Jack');
singlyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(singlyLinked.getHead()); // "Susan"
console.log(singlyLinked.isEmpty()); // false
console.log(singlyLinked.indexOf('Peter')); // 3
console.log(singlyLinked.indexOf('Cris')); // -1
singlyLinked.remove('Tom');
singlyLinked.removeAt(2);
singlyLinked.print(); // "Susan,Jack,Paul"
singlyLinked.list(); // 具体控制台

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。 而双向链表,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

单向链表与双向列表比较
  • 双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。 所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。 虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
  • 双向链表提供了两种迭代列表的方法:从头到尾,或者从尾到头。 我们可以访问一个特定节点的下一个或前一个元素。
  • 在单向链表中,如果迭代链表时错过了要找的元素,就需要回到链表起点,重新开始迭代。
  • 在双向链表中,可以从任一节点,向前或向后迭代,这是双向链表的一个优点。
  • 所以,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。
实现
class Node {
  constructor(element){
    this.element = element; //当前节点的元素
    this.next = null; //下一个节点指针
    this.previous = null; //上一个节点指针
  }
    
}

// 创建双向链表 DoublyLinkedList 类
class DoublyLinkedList {
  constructor(){
  
    this.length = 0; // 链表长度
    this.head = null; // 链表头部
    this.tail = null; // 链表尾部
  }



  // 向链表尾部添加一个新的项
  append (element) {
    var node = new Node(element);
    var currentNode = this.tail;

    // 判断是否为空链表
    if (currentNode === null) {
      // 空链表
      this.head = node;
      this.tail = node;
    } else {
      currentNode.next = node;
      node.prev = currentNode;
      this.tail = node;
    }

    this.length++;
  }

  // 向链表特定位置插入一个新的项
  insert (position, element) {
    if (position < 0 && position > this.length) {
      // 越界
      return false;
    } else {
      var node = new Node(element);
      var index = 0;
      var currentNode = this.head;
      var previousNode;

      if (position === 0) {
        if (!this.head) {
          this.head = node;
          this.tail = node;
        } else {
          node.next = currentNode;
          currentNode.prev = node;
          this.head = node;
        }
      } else if (position === this.length) {
        this.append(element);
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }

        previousNode.next = node;
        node.next = currentNode;

        node.prev = previousNode;
        currentNode.prev = node;
      }

      this.length++;

      return true;
    }
  }

  // 从链表的特定位置移除一项
  removeAt (position) {
    if ((position < 0 && position >= this.length) || this.length === 0) {
      // 越界
      return false;
    } else {
      var currentNode = this.head;
      var index = 0;
      var previousNode;

      if (position === 0) {
        // 移除第一项
        if (this.length === 1) {
          this.head = null;
          this.tail = null;
        } else {
          this.head = currentNode.next;
          this.head.prev = null;
        }
      } else if (position === this.length - 1) {
        // 移除最后一项
        if (this.length === 1) {
          this.head = null;
          this.tail = null;
        } else {
          currentNode = this.tail;
          this.tail = currentNode.prev;
          this.tail.next = null;
        }
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        previousNode.next = currentNode.next;
        previousNode = currentNode.next.prev;
      }

      this.length--;

      return true;
    }
  }

  // 从链表中移除指定项
  remove(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  }

  // 返回元素在链表的索引,如果链表中没有该元素则返回 -1
  indexOf (element) {
    var currentNode = this.head;
    var index = 0;

    while (currentNode) {
      if (currentNode.element === element) {
        return index;
      }

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  }

  // 如果链表中不包含任何元素,返回 true ,如果链表长度大于 0 ,返回 false
  isEmpty () {
    return this.length == 0;
  }

  // 返回链表包含的元素个数,与数组的 this.length 属性类似
  size () {
    return this.length;
  }

  // 获取链表头部元素
  gethead () {
    return this.head.element;
  }

  // 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
  toString() {
    var currentNode = this.head;
    var string = '';

    while (currentNode) {
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  }

  print () {
    console.log(this.toString());
  }

  // 获取整个链表
  list() {
    console.log('this.head: ', this.head);
    return this.head;
  }
}

// 创建双向链表
var doublyLinked = new DoublyLinkedList();
console.log(doublyLinked.isEmpty()); // true
doublyLinked.append('Tom');
doublyLinked.append('Peter');
doublyLinked.append('Paul');
doublyLinked.print(); // "Tom,Peter,Paul"
doublyLinked.insert(0, 'Susan');
doublyLinked.print(); // "Susan,Tom,Peter,Paul"
doublyLinked.insert(1, 'Jack');
doublyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(doublyLinked.gethead()); // "Susan"
console.log(doublyLinked.isEmpty()); // false
console.log(doublyLinked.indexOf('Peter')); // 3
console.log(doublyLinked.indexOf('Cris')); // -1
doublyLinked.remove('Tom');
doublyLinked.removeAt(2);
doublyLinked.print(); // "Susan,Jack,Paul"
doublyLinked.list(); // 请看控制台输出

循环链表

定义

循环链表是一种特殊的单链表。 循环链表和单链表相似,节点类型都是一样。 唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性指向它本身

这种行为会导致链表中每个节点的next属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表 环形链表从任意一个节点开始,都可以遍历整个链表。

实现
 class Node {
    constructor(element){
       this.element = element; // 当前节点的元素
      this.next = null; // 下一个节点指针
    }
   
 }

    // 循环链表
class CircularLinkedList{

  constructor(){
    this.length = 0;
    this.head = null;
  }
 

  append  (element) {
    var node = new Node(element),
      current;

    if (!this.head) {
      this.head = node;
      // 头的指针指向自己
      node.next = this.head;
    } else {
      current = this.head;

      while (current.next !== this.head) {
        current = current.next;
      }

      current.next = node;
      // 最后一个节点指向头节点
      node.next = this.head;
    }

    this.length++;
    return true;
  }

  insert  (position, element) {
    if (position > -1 && position < this.length) {
      var node = new Node(element),
        index = 0,
        current = this.head,
        previous;

      if (position === 0) {
        // 头节点指向自己
        node.next = this.head;
        this.head = node;
      } else {
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        previous.next = node;
        node.next = current;
      }
      this.length++;
      return true;
    } else {
      return false;
    }
  }
  removeAt  (position) {
    if (position > -1 && position < this.length) {
      var current = this.head,
        previous,
        index = 0;
      if (position === 0) {
        this.head = current.next;
      } else {
        while (index++ < position) {
          previous = current;
          current = current.next;
        }
        previous.next = current.next;
      }
      this.length--;
      return current.element;
    } else {
      return false;
    }
  }
  remove  (element) {
    var current = this.head,
      previous,
      indexCheck = 0;
    while (current && indexCheck < this.length) {
      if (current.element === element) {
        if (indexCheck == 0) {
          this.head = current.next;
          this.length--;
          return true;
        } else {
          previous.next = current.next;
          this.length--;
          return true;
        }
      } else {
        previous = current;
        current = current.next;
        indexCheck++;
      }
    }
    return false;
  }
 
  indexOf  (element) {
    var current = this.head,
      index = 0;
    while (current && index < this.length) {
      if (current.element === element) {
        return index;
      } else {
        index++;
        current = current.next;
      }
    }
    return -1;
  }
  isEmpty  () {
    return this.length === 0;
  }
  size  () {
    return this.length;
  }

  // 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
  toString  () {
    var current = this.head,
      string = '',
      indexCheck = 0;
    while (current && indexCheck < this.length) {
      string += ',' + current.element;
      current = current.next;
      indexCheck++;
    }
    return string.slice(1);
  }

  // 获取链表头部元素
  gethead  () {
    return this.head.element;
  }

  // 打印链表数 据
  print  () {
    console.log(this.toString());
  }

  // 获取整个链表
  list  () {
    console.log('this.head: ', this.head);
    return this.head;
  }
}

    // 创建循环链表实例
var circularLinked = new CircularLinkedList();
console.log(circularLinked.removeAt(0)); // false
console.log(circularLinked.isEmpty()); // true
circularLinked.append('Tom');
circularLinked.append('Peter');
circularLinked.append('Paul');
circularLinked.print(); // "Tom,Peter,Paul"
circularLinked.insert(0, 'Susan');
circularLinked.print(); // "Susan,Tom,Peter,Paul"
circularLinked.insert(1, 'Jack');
circularLinked.print(); // "Susan,Jack,Tom,Peter,Paul"


circularLinked.remove('Tom');
circularLinked.removeAt(2);
circularLinked.print(); // "Susan,Jack,Paul"
circularLinked.list(); // 具体控制台

最后

数据结构的思想也在日常的编程中也颇为重要,要做到活学活用, 望各位看官 不吝点赞(前端界的一枚小学生)

参考文章: JavaScript 数据结构与算法之美 - 线性表(数组、栈、队列、链表)

学习JavaScript数据结构(一)——栈和队列