阅读《数据结构与算法 JavaScript 描述 第二版》之链表

77 阅读4分钟

数组的缺点

不同语言对数组的支持是不一样的,此时跳出 JavaScript 数组的范围,因为 JavaScript 数组灵活度很高, 其他语言设计数组灵活度没有这么高(例如:数组的长度是固定的,操作数据也有困难。)。

JavaScript 数组的数组本质:是对象,不是原始类型,是引用类型,操作起来效率相比于其语言可能比较低。

使用链表替代数组

人如果发现数组运行效率的很慢,此时就应该考虑使用 链表。 除去随机访问数据这种情况,其他的情况都可以替代一维数组

链表概述

  • 链表:一组节点的集合。-
  • 链表节点: 通过一个对象的引用指向它的后续
  • 链:指向一个节点的引用
  • 头节点:链表的头部

链表插入删除语言表述

插入

链表在插入一个节点时候效率很高。流程:

  • 当前节点链断开
  • 的节点的前端与当前节点的后端链接
  • 的节点的后度端与当前节点断开后半部分的端链接

删除

  • 指定当前节点
  • 断开当前节点前后链
  • 当前节点的前一个节点的连接当前节点的后一个节点

设计一个基于对象的链表

  • 需要以 Node 节点类和 LLink 类
  • LLink 类在原型链上挂在方法
export function Node(element) {
  this.element = element;
  this.next = null;
}

export function LLink() {
  this.head = new Node("head");
}

LLink.prototype.find = function (item) {
  let currNode = this.head;
  while (currNode.element != item) {
    currNode = currNode.next;
  }

  return currNode;
};

LLink.prototype.insert = function (newElement, item) {
  let newNode = new Node(newElement);
  let current = this.find(item);
  newNode.next = current.next; // 当前链条的后面部分折断,赋值给 newNode.next
  current.next = newNode;
};

LLink.prototype.findPrevious = function (item) {
  let currentNode = this.head;
  while (!(currentNode.next == null) && currentNode.next.element != item) {
    currentNode = currentNode.next;
  }

  return currentNode;
};

LLink.prototype.remove = function (item) {
  let prevNode = this.findPrevious(item);
  if (!(prevNode.next == null)) {
    prevNode.next = prevNode.next.next;
  }
};

LLink.prototype.display = function () {
  let currNode = this.head;
  while (!(currentNode.next = null)) {
    console.log(currNode.next.element);
    currNode = currNode.next;
  }
};
  • 以下是针对 LLink 的节点测试用例:
import { LLink } from "../LinkedList.js";

describe("测试链表", () => {
  it("insert 方法测试", () => {
    let cities = new LLink();
    cities.insert("Conway", "head");
    expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": null,
    },
  },
}
`);

    cities.insert("Russellille", "Conway");
    expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": null,
      },
    },
  },
}
`);

cities.insert("Carlise", "Russellille");
cities.insert("Alma", "Carlise");
expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Carlise",
          "next": Node {
            "element": "Alma",
            "next": null,
          },
        },
      },
    },
  },
}
`)

cities.remove("Carlise")
expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Alma",
          "next": null,
        },
      },
    },
  },
}
`)
  });
});

双向链表

  • 双向链表有 head 头和 null 尾部
  • 链表中元素首尾相连
  • head + 最后一个 Node 都会指向自己的 null

以下使用 Node 节点类 + LList 类实现的双向链表

export function Node(element) {
  this.element = element;
  this.next = null;
  this.previous = null;
}

export function LList() {
  this.head = new Node("head");
}

LList.prototype.find = function (item) {
  let currNode = this.head;
  while (currNode.element != item) {
    console.log(currNode.next.element);
    currNode = currNode.next;
  }

  return currNode
};

LList.prototype.findLast = function () {
  let currNode = this.head;
  while (!(currNode.next == null)) {
    currNode = currNode.next;
  }

  return currNode;
};

LList.prototype.insert = function (newElement, item) {
  let newNode = new Node(newElement);
  let current = this.find(item);
  newNode.next = current.next;
  newNode.previous = current;
  current.next = newNode;
};

LList.prototype.display = function () {
  let currNode = this.head;
  while (!(currNode.next == null)) {
    console.log(currNode.next.element);
    currNode = currNode.next;
  }
};

LList.prototype.remove = function (item) {
  let currNode = this.find(item);
  if (!(currNode.next == null)) {
    currNode.previous.next = currNode.next;
    currNode.next.previous = currNode.previous;
    currNode.next = null;
    currNode.previous = null;
  }
};

LList.prototype.findLast = function () {
  let currNode = this.head;
  while (!(currNode.next == null)) {
    currNode = currNode.next;
  }
  return currNode;
};

LList.prototype.dispReverse = function () {
  let currNode = this.head;
  currNode = this.findLast();
  console.log("-----", currNode, currNode.previous, currNode.previous == null)

  while(!(currNode.previous == null)) {
    console.log("翻转->当前节点元素", currNode.element)
    currNode = currNode.previous;
  }

  return currNode;
};

实现了列表之后,下面是对双向链表的测试用例

import { Node, LList } from "../DoublyLLink.js";

describe("测试双向链表", () => {
  it("insert 方法测试", () => {
    let cities = new LList();
    cities.insert("Conway", "head");
    expect(cities).toMatchInlineSnapshot(`
LList {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": null,
      "previous": [Circular],
    },
    "previous": null,
  },
}
`);

    cities.insert("Russellille", "Conway");
    expect(cities).toMatchInlineSnapshot(`
LList {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": null,
        "previous": [Circular],
      },
      "previous": [Circular],
    },
    "previous": null,
  },
}
`);

    cities.insert("Carlise", "Russellille");
    cities.insert("Alma", "Carlise");
    expect(cities).toMatchInlineSnapshot(`
LList {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Carlise",
          "next": Node {
            "element": "Alma",
            "next": null,
            "previous": [Circular],
          },
          "previous": [Circular],
        },
        "previous": [Circular],
      },
      "previous": [Circular],
    },
    "previous": null,
  },
}
`);

    cities.remove("Carlise");
    expect(cities).toMatchInlineSnapshot(`
LList {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Alma",
          "next": null,
          "previous": [Circular],
        },
        "previous": [Circular],
      },
      "previous": [Circular],
    },
    "previous": null,
  },
}
`);
    cities.dispReverse()
    expect(cities).toMatchInlineSnapshot(`
LList {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Alma",
          "next": null,
          "previous": [Circular],
        },
        "previous": [Circular],
      },
      "previous": [Circular],
    },
    "previous": null,
  },
}
`);
  });
});

使用 Jest 的快照功能查看是符合我们对 LList 以及其原型上的函数的操作的,测试用例通过。

循环链表

循环列表在单向列表的基础上的扩展:

  • 增加的首尾相链接的功能
export function Node(element) {
  this.element = element;
  this.next = null;
}

export function LLink() {
  this.head = new Node("head");
  this.head.next = this.head;
}

LLink.prototype.find = function (item) {
  let currNode = this.head;
  while (currNode.element != item) {
    currNode = currNode.next;
  }

  return currNode;
};

LLink.prototype.insert = function (newElement, item) {
  let newNode = new Node(newElement);
  let current = this.find(item);
  newNode.next = current.next; // 当前链条的后面部分折断,赋值给 newNode.next
  current.next = newNode;
};

LLink.prototype.findPrevious = function (item) {
  let currentNode = this.head;
  while (!(currentNode.next == null) && currentNode.next.element != item) {
    currentNode = currentNode.next;
  }

  return currentNode;
};

LLink.prototype.remove = function (item) {
  let prevNode = this.findPrevious(item);
  if (!(prevNode.next == null)) {
    prevNode.next = prevNode.next.next;
  }
};

LLink.prototype.display = function () {
  let currNode = this.head;
  while (!(currentNode.next = null) && !(currNode.next.element == "head")) {
    console.log(currNode.next.element);
    currNode = currNode.next;
  }
};

单向链表 的基础上,

下面是测试用例, next 中 [Circular] 进行标记循环:

import { LLink } from "../CircularLLink.js";

describe("循环试链表", () => {
  it("insert 方法测试", () => {
    let cities = new LLink();
    cities.insert("Conway", "head");
    expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": [Circular],
    },
  },
}
`);

    cities.insert("Russellille", "Conway");
    expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": [Circular],
      },
    },
  },
}
`);

    cities.insert("Carlise", "Russellille");
    cities.insert("Alma", "Carlise");
    expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Carlise",
          "next": Node {
            "element": "Alma",
            "next": [Circular],
          },
        },
      },
    },
  },
}
`);

    cities.remove("Carlise");
    expect(cities).toMatchInlineSnapshot(`
LLink {
  "head": Node {
    "element": "head",
    "next": Node {
      "element": "Conway",
      "next": Node {
        "element": "Russellille",
        "next": Node {
          "element": "Alma",
          "next": [Circular],
        },
      },
    },
  },
}
`);
  });
});

小结

  • 链表的特性,与数组的区别,以及西更能
  • 基于原型链实现的单向链表,双向链表,循环列表
  • 使用 Jest 对实现的链表数据结构进行快照测试