3.JavaScript数据结构与算法(链表)

1.链表数据结构

链表存储有序的元素集合,但是不同于数组,链表中的元素在内存中并不是连续放置的,每个元素由一个元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成
相对于传统的数组,链表在添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针。在数组中,可以直接访问任何位置的任何元素,而要访问链表中间的一个元素,则需要从表头开始迭代表直到找到所需的元素

创建链表

const defaultEquals = (a, b) => {
  return a === b;
}
export default class LinkedList {
  constructor(equalsFn = defaultEquals) {
    this.count = 0;
    this.head = undefined;
    this.equalsFn = equalsFn;
  }
}

要表示链表中的第一个以及其他元素,需要一个助手类,表示想要添加到链表中的项

export class Node {
  constructor(element) {
    this.element = element;
    this.next = undefined;
  }
}

LinkedList类的方法

push(element): 向链表尾部添加一个新元素
insert(element, position): 向链表的特定位置插入一个新元素
getElementAt(index): 返回链表中特定位置的元素。如果不存在,返回undefined
remove(element): 从链表中移除一个元素
indexOf(elemet): 返回元素在链表中的索引。如果没有则返回-1
removeAt(position): 从链表的特定位置移除一个元素
isEmpty(): 如果链表中不包含任何元素,返回ture
size(): 返回链表中包含元素的个数
toString(): 返回链表的字符串。由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值  

1.向链表尾部添加元素

该方法考虑两种场景
1.链表为空,添加的是第一个元素
2.链表不为空,追加元素

  push(element) {
    const node = new Node(element);
    let current;
    if(this.head == null) {
      this.head = node;
    } else {
      current = this.head;
      while(current.next != null) { // 获得最后一项
        current = current.next;
      }
      current.next = node;
    }
    this.count ++;
  }

注:链表的最后一个节点的下一个元素始终是undefined 或 null
要向链表的尾部,添加一个元素,首先需要找到最后一个元素。链表只有第一个元素的引用,因此需要循环访问列表,直到找到最后一项。 当一个Node实例被创建,它的next指针总是undefined,这会是链表的最后一项

2.从链表中移除元素

从链表中移除元素同样需要考虑两种场景
1.移除第一个元素
2.移除除第一个元素之外的其他元素

  removeAt(index) {
    if(index >= 0 && index < this.count) {
      let current = this.head;
      // 移除第一项
      if(index === 0) {
        this.head = current.next;
      } else {
        let previous;
        for(let i = 0; i < index; i++) {
          previous = current;
          current = current.next;
        }
        // 将previous与current的下一项链接起来;跳过current,从而移除它
        previous.next = current.next;
      }
      this.count -- ;
      return current.element;
    }
    return undefined;
  }

如果想要移除第一个元素,要做的就是让head只想列表的第二个元素。 如果想要移除链表的最后一个或者中间某个元素,需要迭代链表的节点,直到打到目标位置。(current变量总是为对所循环列表的当前元素的引用),同时,还需要一个对当前元素的前一个元素的引用;在迭代达到目标位置后,要做的就是将previous.next 和 current.next 链接起来,当前节点就会被丢弃在计算机内存中,等着被垃圾回收机制清除。

3循环迭代链表到目标位置

  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;
  }

重构remove方法

  removeAt(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;
  }

4.在任意位置插入元素

  insert(element, index) {
    if(index >= 0 && index <= this.count) {
      const node = new Node(element);
      if(index === 0) {
        const current = this.head;
        node.next = current;
        this.head = node;
      } else {
        const previous = this.getElementAt(index - 1);
        const current = previous.next;
        node.next = current;
        previous.next = node;
      }
    }
  }

第一种场景:在链表的起点添加一个元素 current 变量表示对链表中第一个元素的引用,需要做的是把node.next的值设为current(链表中的第一个元素,或简单的设为head),现在head和node.next都指向了current,接下来把head的引用改为node; 第二种场景:在链表的中间或尾部添加一个元素 首先,需要迭代到链表,找到目标位置。这个时候,循环到index-1的位置,标识需要添加节点位置的前一个位置,当跳出循环时,previous将是对想要插入新元素的位置之前的一个元素的引用,current将是想要插入新元素的位置之后的一个元素的引用。此时,需要在previous和current之间添加新的元素。需要把新的元素和当前元素连接起来,然后改变previous和current之间的连接。 如果向最后一个位置添加一个新的元素,previous将是对链表最后一个元素的引用,而current将是undeined,这种情况下,node.next指向current,而previous.next将指向node。

5.返回一个元素的位置

  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;
  }

创建一个变量,辅助循环访问列表,该变量是current,初始值为head,迭代元素,从head开始,直到链表长度为止。 每次迭代,验证current节点元素是否是目标元素相等,若相等,则返回该元素所在位置的索引。

6.从链表中移除元素

  remove(element) {
    const index = this.indexOf(element);
    return this.removeAt(index);
  }

该方法可与removeAt相联系

7.isEmpty、size、getHead方法

 size() {
    return this.count;
  }
  isEmpty() {
    return this.size() === 0;
  }
  getHead() {
    return this.head;
  }

8.toString方法

  toString() {
    if(this.head === null) {
      return '';
    }
    let objString = `${this.head.element}`;
    let current = this.head.next;
    for(let i = 1; i< this.size() && current != null;i++) {
      objString = `${objString},${current.element}`;
      current = current.next;
    }
    return objString;
  }

2.双向链表

双向链表和普通链表的区别在于,在链表的一个节点只有链向下一个节点的链接,而在双向链表中,连表示双向的:一个链向下一个元素,另一个链向前一个元素

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;
  }
}

DoublyLinkedList是一个特殊的LinkedList类,类DoublyLinkedList类将继承LinkedList类中所有的属性和方法,一开始,在DoublyLinkdList的构造函数中,要调用LinkedList的构造函数,它会初始化equalsFn、count和head属性。另外,会保存对链表最后一个元素的引用(tail)
双向链表提供了两种迭代的方法:从头到尾,或者从尾到头。可以访问一个特定节点的下一个或前一个元素。为了实现这种行为,需要追踪每个节点的前一个节点,所以除了Node类中的next属性,DoubleLinkedList会使用一个特殊的系欸但,DoublyNode的节点有一个叫prev的属性。
在单向链表中,如果迭代时错过了要找的元素,就需要回到起点,重新开始迭代。这是双向链表的一个优势

1.在任意位置插入新元素

在双向链表中插入一个新元素跟单向链表非常类型,区别在于,单向链表只要控制一个next指针,而双向链表则要同时控制next和prev这两个指针。

  insert(element, index) {
    if(index >= 0 && index <= this.count) {
      const node = new DoublyNode(element);
      let current = this.head;
      if(index === 0) {
        if(this.head == null) {
          this.head = node;
          this.tail = node;
        } else {
          node.next = this.head;
          current.prev = node;
          this.head = node;
        }
      } else if(index === this.count) {
        current = this.tail;
        current.next = node;
        node.prev = current;
        this.tail = node;
      } else {
        const previous = this.getElementAt(index - 1);
        current = previous.next;
        node.next = current;
        previous.next = node;
        current.prev = node;
        node.prev = previous;      
      }
      this.count ++;
      return true;
    }
    return false;
  }

第一种场景:在双向链表的第一个位置插入一个元素 如果该链表为空,只需要head和tail都指向这个新节点 如果不为空,current变量将是对双向链表中第一个元素的引用。 与单向链表不同之处在于,还需要为指向上一个元素的指针设置一个值,current.prev指针将由undefined指向node,而node.prev指针已经是undefined,无需更新。
第二种场景:在双向链表的最后添加一个新元素 current变量将引用最后一个元素,然后开始简历链接,current.next指向node,node.next指向undefined,node.prev将引用current,同时更新tail,由指向current变为指向node
第三种场景:在双向链表中间插入一个新元素,需要找到当前索引位置的元素,在该元素与下一个元素中间插入元素,node.next将指向current,而previous.next指向node,这样就不会丢失节点之间的链接,然后处理所有链接,current.prev指向node,node.prev指向previous
可对insert和remove这两个方法的实现进行一些该进 1.在结果为否的情况下,可以把元素插入双向链表的尾部。 2.如果position大于length/2,可以从尾部开始迭代,而不是从头开始。

2.从任意位置移除元素

  removeAt(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 {
        current = this.getElementAt(index);
        const previous = current.prev;
        previous.next = current.next;
        current.next.prev = previous;
      }
      this.count --;
      return current.element;
    }
    return undefined;
  }

第一种场景:从头部移除一个元素 current变量为双向链表中第一个元素的引用,是将要移除的元素。 第一步需要改变head的引用,将其改为current.next,然后需要更新current.next指向上一个元素prev的指针为undefined,即head.prev的引用改为undefined。同时检查该链表是不是只有一个元素,如果是,则需要将tail设为undefined
第二种场景:从最后一个位置移除元素 将tail的引用赋给current变量,接下来,把tail的引用更新为双向链表中倒数第二个元素(tail = current.prev),此时只需要将next指针更新为undefined即可(tail.next = null)
第三种场景:从双向链表中间移除一个元素 首先需要迭代双侠那个链表,找到位置,current变量所引用的即为要移除的元素,通过更新previous.next和current.next.prev的引用,在双向链表中跳过它,即previous.next指向current.next,而current.next.prev将指向previous

3.循环链表

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

class CircularLinkedList extends LinkedList {
  constructor(equalsFn = defaultEquals) {
    super(equalsFn);
  }
}

CircularLinkedList 类不需要任何额外的属性,所以直接扩展LinkedList类并覆盖要写的方法即可

1.在任意位置插入新元素

  insert(element, index) {
    if(index >= 0 && index <= this.count) {
      const node = new Node(element);
      let current = this.head;
      if(index === 0) {
        if(this.head == null) {
          this.head = node;
          node.next = this.head;
        } else {
          node.next = current;
          current = this.getElementAt(this.size() - 1);
          this.head = node;
          current.next = this.head;
        }
      } else {
        const previous = this.getElementAt(index - 1);
        node.next = previous.next;
        previous.next = node;
      }
      this.count ++;
      return true;
    }
    return false;
  }

第一种场景:在循环链表的第一个位置插入新元素 如果循环链表为空,就将head赋值为新创建的元素,并且将最后一个节点连接到head 第二种场景:在一个非空循环链表的第一个位置插入元素 将node.next指向head引用的节点,还需要保证最后一个节点指向了这个新的头部元素,所以需要取得最后一个元素的引用,即将头部元素更新为新元素,再将最后一个节点指向新的头部节点 第三种场景:在循环链表中节能插入新元素 与LinkedList类似

2.从任意位置移除元素

  removeAt(index) {
    if(index >= 0 && index < this.count) {
      let current = this.head;
      if(index === 0) {
        if(this.size() === 1) {
          this.head = undefined;
        } else {
          const removed = this.head;
          current = this.getElementAt(this.size());
          this.head = removed.next;
          current.next = this.head;
          current = removed;
        }
      } else {
        const previous = this.getElementAt(index - 1);
        current = previous.next;
        previous.next = current.next;
      }
      this.count --;
      return current.element;
    }
    return undefined;
  }

第一个场景是循环链表只有一个元素,直接将head赋值为undefined 第二种场景是从一个循环链表中移除第一个元素,需要修改最后一个节点的next属性,首先需要保存head元素的引用,同时获取循环链表最后一个节点的引用,将head指向第二个元素,同时将最后一个元素的引用也指向第二个元素

4.有序链表

有序链表是指保持元素有序的链表结果,除了使用排序算法之外,还可以将元素插入正确的位置来保证链表的有序性

const Compare = {
  LESS_THAH: -1,
  BIGGER_THAH: 1
}
function defaultCompare(a, b) {
  if( a === b) {
    return 0;
  }
  return a < b ? Compare.LESS_THAH : Compare.BIGGER_THAH;
}
class SortedLinkedList extends LinkedList {
  constructor(equalsFn = defaultEquals, comparaFn = defaultCompare) {
    super(equalsFn);
    this.comparaFn = comparaFn;
  }
}

SortedLinkedList类会从LinkedList类中继承所有的属性和方法,但是由于这个类有特别的行为,需要一个用来比较的函数,如果元素相同,返回0,如果第一个元素小于第二个元素,返回-1,反之则返回1

1.有序插入元素

  insert(element, index = 0) {
    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.comparaFn(element, current.element)
      if(comp === Compare.LESS_THAH) {
        return i;
      }
      current = current.next;
    }
    return i;
  }

由于不想允许在任何位置插入元素,需要给index设置一个默认值0,以便于直接调用list.insert()而无需传入index参数,这样做可以不用重写LinkedList类的方法,只需要覆盖insert方法的行为。 如果有序链表为空,可以直接调用LinkedList的insert方法并传入0作为index,如果不为空,找到正确的插入位置,并调用LinkedList的方法,传入该位置来保证链表的有序性 通过getIndexNextSortedElement方法,跌打有序链表找到需要插入元素的位置。

5.创建StackLinkedList类

我们还可以使用LinkedList类以及其变种作为内部的数据结构来创建其他数据结构,例如栈、队列和双向队列

class StackLinkedList {
  constructor() {
    this.items = new DoublyLinkedList();
  }
  push(element) {
    this.items.push(element);
  }
  pop() {
    if(this.isEmpty()) {
      return undefined;
    }
    return this.items.removeAt(this.size() - 1);
  }
  peek() {
    if(this.isEmpty()) {
      return undefined;
    }
    return this.items.getElementAt(this.size() - 1).element;
  }
  isEmpty() {
    return this.items.isEmpty();
  }
  size() {
    return this.items.size();
  }
  clear() {
    this.items.clear();
  }
  toString() {
    return this.items.toString();
  }
}

对于StackLinkedList类来说,使用DoublyLinkedList来存储数据,因为对于栈来说,会向链表的尾部添加元素,也会从尾部移除元素,DoublyLinkedList类有最后一个元素的引用,无需迭代整个链表的元素就能获取它。双向链表可以直接获取头尾的元素,减少过程的消耗,时间复杂度和原始的Stack实现相同,为O(1)

你可能感兴趣的:(链表,javascript,数据结构,前端)