线性表存储结构

线性表在内存中的两种数据结构

  • 链式存储
  • 顺序存储,静态,动态

    数组的缺陷

    在很多编程语言中,数组的长度是固定的,所以当数组已被数据填满时,再要加入新的元素就会非常困难。在数组中,添加和删除元素也很麻烦,因为需要将数组中的其他元素向前或向后平移,以反映数组刚刚进行了添加或删除操作。

JavaScript 中数组的主要问题是,它们被实现成了对象,与其他语言的数组相比,效率很低。除了对数据的随机访问,链表几乎可以用在任何可以使用一维数组的情况中。

链表的基本概念

  • 为了表示数据元素与后继数据元素之间的逻辑关系,对数据元素来说不仅要存储本身的数据还要存储后继元素的位置。
  • 分别称为数据域和指针域,两部分合起来称为结点
  • 数据元素就是结点,一组结点组成的集合称为数据对象
  • 我们把第一个结点的位置叫做头指针,为了更加方便的操作链表会在单链表的第一个结点前附加一个结点称为头结点,头结点的数据域不存储任何东西,头结点可以没有。

链表是由一组结点组成的集合。每个结点都使用一个对象的引用指向它的后继。指向另一 个结点的引用叫做链。(一个变量存了其他变量的地址就叫对其他变量的引用)

头结点和头指针

  • 有了头结点,对第一个元素结点前插入结点和删除第一结点的操作和其他操作统一了,通常是一个值为 null 的节点
  • 头结点不是必要要素
  • 链表中第一个结点的存储位置叫做头指针

    链表的基本操作

  • 找到值对应的结点 find(item)

  • 找到值对应的前一个结点 findPrevious(item)
  • 在值后面插入结点 insert(element,item)
  • 移除值对应的结点 remove(item)

    链表的JavaScript实现

    每一个node为一个对象,因为js语言的实现,对象赋值,赋值的是地址

    1. function Node(element) {
    2. this.element = element;
    3. this.next = null;
    4. }
    5. function LinkedList(head) {
    6. this.head = new Node(head);
    7. this.find = find;
    8. this.insert = insert;
    9. this.remove = remove;
    10. this.findPrevious = findPrevious;
    11. this.travel = travel()
    12. }
    13. function find(item) {
    14. let currNode = this.head;
    15. while (currNode.element !== item) {
    16. currNode = currNode.next;
    17. }
    18. return currNode;
    19. }
    20. function insert(element, item) {
    21. let newNode = new Node(element);
    22. let current = this.find(item);
    23. newNode.next = current.next;
    24. current.next = newNode;
    25. }
    26. function remove(item) {
    27. let preNode = this.findPrevious(item);
    28. if (preNode) {
    29. preNode.next = preNode.next.next;
    30. }
    31. }
    32. function findPrevious(item) {
    33. let currNode = this.head;
    34. while (currNode.next && currNode.next.element !== item) {
    35. currNode = currNode.next;
    36. }
    37. return currNode;
    38. }
    39. function travel(callback) {
    40. let currNode = this.head;
    41. while (currNode !== null) {
    42. callback(currNode);
    43. currNode = currNode.next;
    44. }
    45. }

    双向链表

    管从链表的头结点遍历到尾结点很简单,但反过来,从后向前遍历则没那么简单。通过 给 Node 对象增加一个属性,该属性存储指向前驱结点的链接。

    1. function DbNode(element) {
    2. this.element = element;
    3. this.next = null;
    4. this.previous = null;
    5. }
    6. function DbLinkedList(head) {
    7. this.head = new DbNode(head);
    8. this.insert = insert;
    9. this.find = find;
    10. this.remove = remove;
    11. }
    12. function insert(element, item) {
    13. let newNode = new DbNode(element);
    14. let current = find(item);
    15. newNode.next = current.next;
    16. newNode.previous = current;
    17. current.next = newNode;
    18. }
    19. function find(item) {
    20. let currNode = this.head;
    21. while (currNode.element !== item) {
    22. currNode = currNode.next;
    23. }
    24. return currNode;
    25. }
    26. function remove(item) {
    27. let currNode = find(item);
    28. if (currNode !== null) {
    29. currNode.previous.next = currNode.next;
    30. currNode.next.previous = currNode.previous;
    31. }
    32. }

    循环链表

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

    1. function CircularLinkedList(head) {
    2. this.head = new Node(head);
    3. this.head.next = this.head;
    4. }

    加一行代码就可以解决,travel方法会死循环,remove方法也要考虑到移除头结点要考虑到this.head始终存在的问题。

    循环链表解决实际问题

    传说在公元 1 世纪的犹太战争中,犹太历史学家弗拉维奥·约瑟夫斯和他的 10 个同胞 被罗马士兵包围。犹太士兵决定宁可自杀也不做俘虏,于是商量出了一个自杀方案。他 们围成一个圈,从一个人开始,数到第三个人时将第三个人杀死,然后再数,直到杀光 所有人。约瑟夫和另外一个人决定不参加这个疯狂的游戏,他们快速地计算出了两个位 置,站在那里得以幸存。写一段程序将 n 个人围成一圈,并且第 m 个人会被杀掉,计算 一圈人中哪两个人最后会存活。使用循环链表解决该问题。

    1. function Node(element) {
    2. this.element = element;
    3. this.next = null;
    4. }
    5. function LinkedList(head) {
    6. this.head = new Node(head);
    7. this.head.next = this.head;
    8. this.find = find;
    9. this.insert = insert;
    10. this.remove = remove;
    11. this.findPrevious = findPrevious;
    12. this.travel = travel;
    13. this.advance = advance;
    14. }
    15. function find(item) {
    16. let currNode = this.head;
    17. while (currNode.element !== item) {
    18. currNode = currNode.next;
    19. }
    20. return currNode;
    21. }
    22. function insert(element, item) {
    23. let newNode = new Node(element);
    24. let current = this.find(item);
    25. newNode.next = current.next;
    26. current.next = newNode;
    27. }
    28. function remove(item) {
    29. let preNode = this.findPrevious(item);
    30. if (preNode) {
    31. if (this.find(item) === this.head) {
    32. this.head = this.head.next;
    33. preNode.next = this.head;
    34. return;
    35. }
    36. preNode.next = preNode.next.next;
    37. }
    38. }
    39. function findPrevious(item) {
    40. let currNode = this.head;
    41. while (currNode.next && currNode.next.element !== item) {
    42. currNode = currNode.next;
    43. }
    44. return currNode;
    45. }
    46. function travel(callback) {
    47. let currNode = this.head;
    48. do {
    49. console.log(currNode.element);
    50. callback(currNode);
    51. currNode = currNode.next;
    52. } while (currNode.element !== this.head.element);
    53. }
    54. function advance(n) {
    55. let currNode = this.head;
    56. while (n > 0) {
    57. currNode = currNode.next;
    58. n = n - 1;
    59. }
    60. return currNode;
    61. }
    62. function code() {
    63. let list = new LinkedList(1);
    64. let length = 1;
    65. let n = 2;
    66. for (let i = 1; i < 10; i++) {
    67. list.insert(i + 1, i);
    68. length += 1;
    69. }
    70. while (length > 3) {
    71. list.remove(list.advance(n).element);
    72. n += 2;
    73. length -= 1;
    74. }
    75. }
    76. code();

    引入头结点

    1. function LinkedList() {
    2. this.head = new Node(new Symbol());
    3. }

    删除第一个元素结点也统一了,在引入头结点之前没有办法做到删除第一个结点,具体看remove代码,现在我们可以了因为this.head不会永远不会被删除。

    整表删除

  • 在c语言中没有垃圾回收机制,只能手动删除或者程序结束回收

  • 但是我们浏览器有回收机制,对象不被引用就被回收了,把代表链表的变量赋值为null,直接清零。
  • a = new LinkedList,a并不是头结点,a.head才是头结点,一般来说a和a.head都是不会被删除的。

    链表技巧

  • 有时候需要三个指针,pre,cur,next

  • 创建空的头结点