链表

一、链表

要存储多个元素,数组(或列表)可能是最常用的数据结构。这种数据结构非常方便,提供了一个便利的[]语法来访问其元素。然而,这种数据结构有一个缺点:(在大多数语言中)数组的大小是固定的,从数组的起点或中间插入或移除项的成本很高,因为需要移动元素。(尽管我们已经学过,JavaScript 有来自 Array 类的方法可以帮我们做这些事,但背后的情况同样如此。)
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。在数组中,我们可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,则需要从起点(表头)开始迭代链表直到找到所需的元素。

现实中链表的例子:一列火车是由一系列车厢(也称车皮)组成的。每节车厢或车皮都相互连接。你很容易分离一节车皮,改变它的位置、添加或移除它。每节车皮都是链表的元素,车皮间的连接就是指针。

下面来实现一个链表:

/*
*为了复用,用Node类表示我们想要添加到链表中的项。
*它包含一个 element 属性,该属性表示要加入链表元素的值;以及一个 next 属性,该属性是指向链表中下一个元素的指针。
*/
 class Node { 
    constructor(element) { 
      this.element = element; 
      this.next = undefined; 
    } 
  } 
//默认比较函数
function defaultEquals(a, b) { 
 return a === b; 
} 
//链表类需要实现的一些方法
/*
*push(element):向链表尾部添加一个新元素。
*insert(element, position):向链表的特定位置插入一个新元素。
*getElementAt(index):返回链表中特定位置的元素。如果链表中不存在这样的元素,则返回 undefined。
*remove(element):从链表中移除一个元素。
*indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1。
*removeAt(position):从链表的特定位置移除一个元素。
*isEmpty():如果链表中不包含任何元素,返回 true,如果链表长度大于 0则返回 false。
*size():返回链表包含的元素个数,与数组的 length 属性类似。
*toString():返回表示整个链表的字符串。由于列表项使用了 Node 类,就需要重写继
*承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。
*/

class LinkedList { 
    constructor(equalsFn = defaultEquals) { 
      this.count = 0; // {2} 
      this.head = undefined; // {3}  
      this.equalsFn = equalsFn; // {4} 
    }
/*向 LinkedList 对象尾部添加一个元素时,可能有两种场景:链表为空,添加的是第一个
元素;链表不为空,向其追加元素。*/
    push(element) { 
      let node = new Node(element);
      if (this.count === 0) {
        this.head = node
      } else {
        let current = this.head;
        while(current.next != null) {
          current = current.next
        }
        current.next = node
      }
      this.count++
    }
 /*和 push 方法一样,对于从链表中移除元素也存在两种场景:第一
种是移除第一个元素,第二种是移除第一个元素之外的其他元素。*/
    removeAt(index) {
      //移除index的边界限制
      if (index >= 0 && index < this.count) {
        let current = this.head;
        //移除第一项的
        if (index === 0) {
          this.head = current.next;
        } else {
          let previous = this.getElementAt(index-1);
          current = previous.next
          // 将 previous 与 current 的下一项链接起来:跳过 current,从而移除它
          previous.next = current.next;
        }
        this.count--
        return current.element
      }
      return undefined
    }
    getElementAt(index) { 
      if (index >= 0 && index <= this.count) { 
        let node = this.head; 
        for (let i = 0; i < index && node != null; i++) { 
          node = node.next; 
        } 
        return node; 
      } 
      return undefined;
    }
    insert(element, index) {
      //插入index边界限制
      if(index>=0 && index <= this.count) {
        let node = new Node(element);
        let current;
        //插入到第一位
        if (index === 0) {
          current = this.head;
          node.next = current;
          this.head = node;
        } else {
          let previous = this.getElementAt(index-1);
          current = previous.next;
          node.next = current;
          previous.next = node
        }
        this.count++
        return true
      }
      return false
    }
    indexOf(element) {
      let current = this.head;
      for(let i=0; i<this.count && current != null; i++) {
        if (this.equalsFn(element, current.element)) {
          return i
        }
        current = current.next
      }
      return -1
    }
    remove(element) {
      let index = this.indexOf(element);
      return this.removeAt(index)
    }
    toString() {
      if (this.head == null) {
        return ''
      }
      let current = this.head;
      let str = current.element;
      for(let i=1; i<this.count && current != null; i++) {
        current = current.next;
        str +=  `,${current.element}`
      }
      return str
    }
  }

二、双向链表:双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。双向链表提供了两种迭代的方法:从头到尾,或者从尾到头。我们也可以访问一个特定节点的下一个或前一个元素。

//通过扩展前面的Node类和LinkedList 来实现双向链表
class DoublyNode extends Node {
  constructor(element, next, prev) { 
    super(element, next);
    this.prev = prev; 
  } 
}

class DoublyLinkedList extends LinkedList { 
  constructor(equalsFn = defaultEquals) { 
    super(equalsFn); 
    this.tail = undefined;
  }
  insert(element, index) {
    //插入index边界限制
    if(index>=0 && index <= this.count) {
      let node = new DoublyNode(element);
      let current = this.head;
      //插入到第一位
      if (index === 0) {
        if (this.count === 0) {
          //没有元素的时候插入头部 
          this.head = node
          this.tail = node 
        } else {
          current.prev = node;
          node.next = current;
          this.head = node;
        }
      } else if (index === this.count){
        //插入到最后位置
        current = this.tail;
        current.next = node;
        node.prev = current;
        this.tail = node;
      } else {
        //插入到中间位置
        let previous = this.getElementAt(index-1);
        current = previous.next;
        node.next = current;
        node.pre = previous;
        current.prev = node;
        previous.next = node;
      }
      this.count++
      return true
    }
    return false
  }
  removeAt(index) {
    //移除index边界限制
    if (index >= 0 && index < this.count) {
      let current = this.head;
      //移除第一项的
      if (index === 0) {
        this.head = current.next; 
        if (this.count === 1) {
          //只有一项的时候
          this.tail = undefined
        } else {
          //有多项的时候
          this.head.prev = undefined;
        }
      } else if (index === this.count - 1) {
        //移除最后一项时
        current = this.tail; 
        this.tail = current.prev;
        this.tail.next = undefined;
      } else {
        //移除中间项时
        let previous = this.getElementAt(index-1);
        current = previous.next;
        previous.next = current.next;
        current.next.prev = previous;
      }
      this.count--
      return current.element
    }
    return undefined
  }
} 

三、循环链表:1)循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用undefined,而是指向第一个元素(head)。
2)双向循环链表有指向 head 元素的 tail.next 和指向 tail 元素的 head.prev。

由于和链表和双向链表没有·多大改动在此不作举例。

四、有序链表:有序链表是指保持元素有序的链表结构。除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。

const Compare = { 
  LESS_THAN: -1, 
  BIGGER_THAN: 1 
}; 
function defaultCompare(a, b) { 
  if (a === b) { 
    return 0; 
  } 
  return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
/*如果用于比较的元素更复杂一些,我们可以创建自定义的
比较函数并将它传入 SortedLinkedList 类的构造函数中。*/
class SortedLinkedList extends LinkedList { 
  constructor(equalsFn = defaultEquals, compareFn = defaultCompare) { 
    super(equalsFn); 
    this.compareFn = compareFn; 
  }
  insert(element) {
    if (this.isEmpty()) { 
        return super.insert(element, 0);
    } 
    const pos = this.getIndexNextSortedElement(element);
    return super.insert(element, pos);
  } 
  getIndexNextSortedElement(element) { 
    let current = this.head; 
    let i = 0; 
    for (; i < this.size() && current; i++) { 
      const comp = this.compareFn(element, current.element);
      if (comp === Compare.LESS_THAN) { 
        return i; 
      } 
      current = current.next; 
    } 
    return i;
  } 
}

本内容整理自《学习JavaScript数据结构与算法》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值