遇事不决就用数组?面试官: 知道链表吗

191 阅读5分钟

前言

日常开发中我们用的最多的应该就是数组了,数组可以满足我们大部分的业务开发要求,但是如果是一些对性能要求高的工具或者是框架实现来说,数组可能并不是一个最佳的方案,比如react的最新实现就是用了链表

数组

定义

是一个连续的结构,靠下标查找速度会比较快,时间复杂度O(1)

特点

插入和删除会比较慢,因为需要移动元素,时间复杂度O(N)

链表

定义

是一个不连续的结构,只能遍历查找速度会比较慢,时间复杂度O(N)

特点

插入和删除会比较快,不需要移动元素,只需要考虑相邻结点的指针改变,时间复杂度O(1)

单向链表

单向链表的结构大概是这样的

{
    element: first,
    next: {
        element: second,
        next: {
            element: third,
            next: null
        }
    }
}

实现

class LinkList {
    constructor() {

    }
    #head = null;
    #length = 0;
    #Node = class{
        constructor(element) {
            this.element = element;
            this.next = null;
        }
    }

    append = (element) => {
        const node = new this.#Node(element);
        let currentNode = this.#head;
        if(this.#head){
            while(currentNode.next){
                currentNode = currentNode.next;
            }
            currentNode.next = node;
        } else{
            this.#head = node;
        }
        this.#length++;
    }

    insert = (position, element) => {
        if(position < 0 || position >= this.#length){
            return false;
        }
        const node = new this.#Node(element);
        let previousNode;
        let currentNode = this.#head;
        
        if(position === 0){
            node.next = this.#head;
            this.#head = node;
        }
        for(let i = 0; i < position; i++){
            previousNode = currentNode;
            currentNode = currentNode.next;
        }

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

        this.#length ++;
    }

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

                if (position === 0) {
                        this.#head = currentNode.next;
                } else {
                        // 循环找到位置

    for(let i = 0; i < position; i++){
        previousNode = currentNode;
        currentNode = currentNode.next;
    }
                        // 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
    previousNode.next = currentNode.next;
                }

                this.#length--;
        }
    };

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

    list = () => {
        return this.#head;
    };

    // 由于链表使用了 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());
    };


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

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

                index++;
                currentNode = currentNode.next;
        }

        return -1;
    };
}

const linkList = new LinkList();

linkList.append('Tom');
linkList.append('Peter');
linkList.append('Paul');

linkList.insert(1, 'Jack');
linkList.print(); // "Tom,Jack,Peter,Paul"

双向链表

双向链表的结构大概是这样的,其实和单向链表差不多

{
    element: first,
    next: {
        element: second,
        next: {
            element: third,
            next: null
        },
        prev: {
            element: first,
            next: {
                element: second,
                next: {
                    element: third,
                    next: null
                },
                prev: ...
            }
        },
    },
    prev: null,
}

实现

class DoubleLinkList {
  constructor() {

  }
  #head = null;
  #length = 0;
  #tail = null;
  #Node = class {
    constructor(element) {
      this.element = element;
      this.next = null;
      this.previous = null; //上一个节点指针
    }
  }

  append = (element) => {
    const node = new this.#Node(element);
    let currentNode = this.#tail;
    if (currentNode) {
      currentNode.next = node;
      node.prev = currentNode;
      this.#tail = node;
    } else {
      this.#head = node;
      this.#tail = node;
    }
    this.#length++;
  }

  insert = (position, element) => {
    if (position < 0 || position > this.#length) {
      return false;
    }
    const node = new this.#Node(element);
    let previousNode;
    let currentNode = this.#head;

    if (position === 0) {
      if (this.#head) {
        node.next = currentNode;
        currentNode.prev = node;
        this.#head = node;
      } else {
        node.next = this.#head;
        this.#head = node;
      }
    } else if (position === length) {
      this.append(element);
    } else {
      for (let i = 0; i < position; i++) {
        previousNode = currentNode;
        currentNode = currentNode.next;
      }
      previousNode.next = node;
      node.next = currentNode;

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

    this.#length ++;
  }

  // 从链表的特定位置移除一项
  removeAt = (position) => {
    if ((position < 0 && position >= this.#length) || this.#length === 0) {
      // 越界
      return false;
    } else {
      let currentNode = this.#head;
      let 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 {
        for (let i = 0; i < position; i++) {
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
      previousNode.next = currentNode.next;
      previousNode = currentNode.next.prev;
    }

    this.#length--;

    return true;
  }
}


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

  list = () => {
    return this.#head;
  };

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

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

    return string.slice(1);
  };

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


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

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

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };
}


const doubleLinkList = new DoubleLinkList();

doubleLinkList.append('Tom');
doubleLinkList.append('Peter');
doubleLinkList.append('Paul');

doubleLinkList.insert(1, 'Jack');  // "Tom,Jack,Peter,Paul"

单向链表与双向链表比较

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

循环链表

循环链表的结构大概是这样的,就是尾部会指回头部形成一个闭环

{
    element: first,
    next: {
        element: second,
        next: {
            element: first,
            next: {
                element: second,
                next: {
                    element: first,
                    next: {
                        element: second,
                        next: ...
                    }
                }
            }
        }
    }
}

实现

class CircularLinkList {
  constructor() {

  }
  #head = null;
  #length = 0;
  #Node = class{
      constructor(element) {
          this.element = element;
          this.next = null;
      }
  }

  append = (element) => {
      const node = new this.#Node(element);
      let currentNode = this.#head;
      if(this.#head){
          while(currentNode.next !== this.#head){
              currentNode = currentNode.next;
          }
          currentNode.next = node;
          node.next = this.#head;
      } else{
          this.#head = node;
          node.next = this.#head;
      }
      this.#length++;
  }

  insert = (position, element) => {
      if(position < 0 || position >= this.#length){
          return false;
      }
      const node = new this.#Node(element);
      let previousNode;
      let currentNode = this.#head;
      
      if(position === 0){
          node.next = this.#head;
          this.#head = node;
      }
      for(let i = 0; i < position; i++){
          previousNode = currentNode;
          currentNode = currentNode.next;
      }

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

      this.#length ++;
  }

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

      if (position === 0) {
        this.#head = currentNode.next;
      } else {
        // 循环找到位置

            for(let i = 0; i < position; i++){
                previousNode = currentNode;
                currentNode = currentNode.next;
            }
        // 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
            previousNode.next = currentNode.next;
      }

      this.#length--;
    }
  };

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

  list = () => {
      return this.#head;
  };

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

    for(let i = 0; i < this.#length && currentNode; i++){
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  };

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


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

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

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };
}

const circularLinkList = new CircularLinkList();

circularLinkList.append('Tom');
circularLinkList.append('Peter');
circularLinkList.append('Paul');

circularLinkList.insert(1, 'Jack');
circularLinkList.print(); // "Tom,Jack,Peter,Paul"

总结

为什么面试官喜欢问链表,其实链表的实现挺难的,很多地方都是依赖同一个指针,很容易就会出现bug,想彻底掌握只有多写多练,没什么其他的捷径了