参考资料:

存储方式

数据结构的存储方式只有两种:顺序存储(数组)、链式存储(链表)

名称 优点 缺点
数组 连续存储、随机访问o(1) 需要扩容、插入o(n)、删除o(n)
链表 无需扩容、知道位置则插入删除o(1) 不连续存储无法随机访问o(n)

其他数据结构则是基于这两种存储方式的“上层建筑”:

名称 顺序存储 链式存储
队列 数组实现 链表实现
数组实现 链表实现
邻接矩阵 邻接表
链表实现
散列表 拉链法 线性探查法

线性表

api:初始化,判空,尾部插入,插入,删除,按值查找,按下标查找,获取长度

顺序表实现

  1. class ListByArray {
  2. constructor() {
  3. this.length = 0;
  4. this.arr = [];
  5. }
  6. indexOf(target) {
  7. for (let i = 0; i < this.length; i++) {
  8. if (this.arr[i] == target) return i;
  9. }
  10. return -1;
  11. }
  12. lastIndexOf(target) {
  13. for (let i = this.length - 1; i >= 0; i++) {
  14. if (this.arr[i] === target) return i;
  15. }
  16. return -1;
  17. }
  18. getLength() {
  19. return this.length;
  20. }
  21. getElement(position) {
  22. if (position < 0 || position >= this.length) return false;
  23. return this.arr[position];
  24. }
  25. insert(position, element) {
  26. if (position < 0 || position >= this.length) return false;
  27. for (let i = this.length; i > position; i--) {
  28. this.arr[i] = this.arr[i - 1];
  29. }
  30. this.arr[position] = element;
  31. this.length += 1;
  32. return this.arr;
  33. }
  34. delete(position) {
  35. if (position < 0 || position >= this.length) return false;
  36. let currentItem = this.arr[position];
  37. for (let i = position + 1; i < this.length; i++) {
  38. this.arr[i - 1] = this.arr[i];
  39. }
  40. this.length -= 1;
  41. return currentItem;
  42. }
  43. append(element) {
  44. this.arr[this.length] = element;
  45. this.length += 1;
  46. return true;
  47. }
  48. }

链表实现(单链表)

参考链接:JavaScript数据结构——链表的实现与应用
单链表的结点结构:
image.png
单链表根据有无头结点分为带头结点的单链表、不带头结点的单链表:
image.png
引入头结点的两个优点:1. 对头结点的操作如插入、删除操作与其它结点一致;
API:按索引查找、尾部插入、插入、删除、按值查找、判空

  1. class Node { // 首先需要一个辅助类,用来描述链表中的节点
  2. constructor(val) {
  3. this.val = val;
  4. this.next = null;
  5. }
  6. }
  7. class LinkedList {
  8. constructor() {
  9. this.length = 0; // 长度
  10. this.head = null; // 头
  11. }
  12. getElement(position) {
  13. // 考虑所给索引是否超出范围,然后遍历即可,先实现这个,是其他的基础
  14. if (position < 0 || position >= this.length) return null;
  15. let current = this.head;
  16. for (let i = 0; i < position; i++) {
  17. current = current.next;
  18. }
  19. return current;
  20. }
  21. append(element) {
  22. // 如果链表为空和不为空的情况
  23. let newNode = new Node(element);
  24. if (this.head === null) this.head = newNode;
  25. else {
  26. let lastNode = this.getElement(this.length - 1);
  27. lastNode.next = newNode;
  28. }
  29. this.length++;
  30. }
  31. isEmpty() {
  32. // 判断是否为空
  33. if (this.length == 0) return true;
  34. return false;
  35. }
  36. insert(position, element) {
  37. // 所给position不能超出边界值;再考虑是否插入的是首节点
  38. if (position < 0 || position >= this.length) return false;
  39. let newNode = new Node(element);
  40. if (position === 0) {
  41. newNode.next = this.head;
  42. this.head = newNode;
  43. } else {
  44. let preNode = this.getElement(position - 1);
  45. newNode.next = preNode.next;
  46. preNode.next = newNode.next;
  47. }
  48. this.length++;
  49. return true;
  50. }
  51. remove(position) {
  52. // 是否超出范围;再考虑是否删除的首节点
  53. if (position < 0 || position >= this.length) return false;
  54. if (position === 0) {
  55. this.head = this.head.next;
  56. } else {
  57. let preNode = this.getElement(position - 1);
  58. this.preNode.next = this.preNode.next.next;
  59. }
  60. this.length--;
  61. return true;
  62. }
  63. indexOf(element) {
  64. let current = this.head;
  65. for (let i = 0; i < this.length; i++) {
  66. if (current.val == element) {
  67. return i;
  68. } else {
  69. current = current.next;
  70. }
  71. }
  72. return -1;
  73. }
  74. }

链表遍历

  1. function traverse_iteration(head) {
  2. // 链表的迭代遍历1
  3. for (let p = head; p != null; p = p.next) {
  4. console.log(p.val);
  5. }
  6. }
  7. function traverse_iteration(head) {
  8. // 链表的迭代遍历2
  9. while(head){
  10. console.log(head.val);
  11. head = head.next;
  12. }
  13. function traverse_recurtion(head) {
  14. // 链表的递归遍历
  15. if (!head) {
  16. console.log(head.val);
  17. traverse_recurtion(head.next);
  18. }
  19. }

例题

寻找中间结点(快慢指针)

  1. getMiddleNode() {
  2. let fast = this.head;
  3. let slow = this.head;
  4. while (fast && fast.next && fast.next.next) {
  5. fast = fast.next.next;
  6. slow = slow.next;
  7. }
  8. return slow;
  9. }

链表中是否含有环

  1. hasCycle() {
  2. let fast = this.head;
  3. let slow = this.head;
  4. while (fast && fast.next && fast.next.next) {
  5. fast = fast.next.next;
  6. slow = slow.next;
  7. if (fast == slow) {
  8. return true;
  9. }
  10. }
  11. return false;
  12. }

含有环返回环的起始位置
先让快慢指针相遇,再让其中一指针回到起点,两者再以相同速度前进,再次相遇即起始。

  1. getCycleStart() {
  2. let fast = this.head;
  3. let slow = this.head;
  4. while (fast && fast.next && fast.next.next) {
  5. fast = fast.next.next;
  6. slow = slow.next;
  7. if (fast == slow) break;
  8. }
  9. slow = this.head;
  10. while (slow && slow.next) {
  11. slow = slow.next;
  12. fast = fast.next;
  13. if (slow == fast) return slow;
  14. }
  15. }

寻找链表倒数第k个元素
两个指针,让一指针先走K步,然后相同速度走,当快指针到头返回慢指针。

  1. getLastK(k) {
  2. let fast = this.head;
  3. let slow = this.head;
  4. let i = 0;
  5. while (i < k && fast && fast.next) {
  6. i++;
  7. fast = fast.next;
  8. }
  9. if (i != k) return false;
  10. while (fast) {
  11. fast = fast.next;
  12. slow = slow.next;
  13. }
  14. return slow;
  15. }

循环单链表

将原来单链表末结点next原本的指向为null,改为指向头结点。
数据结构 - 图3
判空:

  1. if(head === head.next)

双链表

双联表的节点结构:
image.png
即一个数据域,两个指针域:
image.png

循环双链表

将双链表头结点前驱指针指向末结点,末结点后驱指针指向头结点。
数据结构 - 图6

静态链表

即用顺序表来实现链表,即数组中保存着一个个结点对象,对象结构如下:

  1. class Node {
  2. constructor(val) {
  3. this.val = val;
  4. this.next = null; // next指向为结点的下标
  5. }
  6. }

api:栈顶添加元素append、返回栈顶元素top、弹出栈顶pop。

链表实现栈

  1. class Node {
  2. constructor(val) {
  3. this.val = val;
  4. this.next = null;
  5. }
  6. }
  7. class StackByLink {
  8. constructor() {
  9. this.length = 0;
  10. this.tophead = null;
  11. }
  12. append(element) {
  13. let newNode = new Node(element);
  14. if (this.tophead == null) this.tophead = newNode;
  15. else {
  16. newNode.next = this.tophead;
  17. this.tophead = newNode;
  18. }
  19. this.length++;
  20. }
  21. pop() {
  22. if (this.length == 0) return false;
  23. let current = this.tophead;
  24. this.tophead = this.tophead.next;
  25. this.length --;
  26. return current;
  27. }
  28. top() {
  29. return this.tophead;
  30. }
  31. }

顺序表实现栈

  1. class StackByArr {
  2. constructor() {
  3. this.length = 0;
  4. this.arr = [];
  5. }
  6. append(element) {
  7. this.arr[this.length++] = element;
  8. }
  9. pop() {
  10. if (this.length == 0) return false;
  11. let current = this.arr[this.length - 1];
  12. delete this.arr[this.length - 1];
  13. this.length--;
  14. return current;
  15. }
  16. top() {
  17. return this.arr[this.length - 1];
  18. }
  19. }

队列

链表实现队列

  1. class QueueByLink {
  2. constructor() {
  3. this.length = 0;
  4. this.prehead = null;
  5. this.bachead = null;
  6. }
  7. isEmpty() {
  8. if (this.length === 0) return true;
  9. return false;
  10. }
  11. append(element) {
  12. let newNode = new Node(element);
  13. if (this.length === 0) {
  14. this.prehead = this.bachead = newNode;
  15. } else {
  16. this.bachead.next = newNode;
  17. this.bachead = newNode;
  18. }
  19. this.length++;
  20. }
  21. shift() {
  22. if (this.isEmpty()) return false;
  23. let current = this.prehead;
  24. this.prehead = this.prehead.next;
  25. this.length--;
  26. return current.val;
  27. }
  28. }

循环队列

数据结构 - 图7
注意点:如何判断队空还是队满,因为此时都有front=rear。有两种方式处理:

  1. 增加一个标识flag:初始时为false,当指针front变动时,flag为false;当rear变动时,flag为true。当front=rear且flag=true时队列满,当front=rear且flag=false时队列空。
  2. 增加一个计数count:初始为0,当rear变动则count+1,front变动count-1但不小于0;当count=队列长度则队列满,count为0队列空。
  3. 少用一个元素空间并规定rear=front为队列空,rear下一位置若为front则队列满。

(在rear处添加元素后,rear指针往后移动一格,所以rear指针所指的元素值为空)
PS:先实现是否队空、队满。
image.png

顺序表实现

  1. class CircularQueue {
  2. constructor(MaxSize = 3) {
  3. this.MaxSize = MaxSize;
  4. this.front = 0;
  5. this.back = 0;
  6. this.flag = false;
  7. this.arr = [];
  8. }
  9. //判断是否为空队列
  10. isEmpty() {
  11. if (this.front == this.back && this.flag == false) return true;
  12. return false;
  13. }
  14. //判断队列是否已满
  15. isFull() {
  16. if (this.front == this.back && this.flag == true) return true;
  17. return false;
  18. }
  19. //返回队列元素个数
  20. queueNums() {
  21. let nums = null;
  22. if (this.back - this.front) nums = this.back - this.front;
  23. else nums = this.MaxSize - (this.front - this.back);
  24. return nums;
  25. }
  26. //返回队列头部元素
  27. top() {
  28. if (this.isEmpty()) return false;
  29. return this.arr[this.front];
  30. }
  31. //队尾插入元素
  32. append(element) {
  33. if (!this.isFull()) {
  34. this.arr[this.back] = element;
  35. this.flag = true;
  36. if (++this.back === this.MaxSize) {
  37. this.back = this.back % this.MaxSize;
  38. }
  39. return true;
  40. }
  41. return "队列已满";
  42. }
  43. //队头删除元素
  44. shift() {
  45. if (!this.isEmpty()) {
  46. let current = this.arr[this.front];
  47. this.arr[this.front] = null;
  48. this.flag = false;
  49. if (++this.front === this.MaxSize) {
  50. this.front = this.front % this.MaxSize;
  51. }
  52. return current
  53. }
  54. return "队列已空";
  55. }
  56. //根据值查找索引
  57. }

树的遍历

前序、中序、后续遍历;n叉树递归遍历

  1. function tree_recurtion(root) {
  2. // “前序遍历”
  3. tree_recurtion(root.left);
  4. // “中序遍历”
  5. tree_recurtion(root.right);
  6. // “后序遍历”
  7. }
  8. function tree_recurtion_n(root) {
  9. // n叉树的递归遍历
  10. for (node of root) {
  11. tree_recurtion_n(node);
  12. }
  13. }

图示:
image.png
前序:1-2-4-6-7-3-5
中序:4-6-7-2-1-5-3
后序:7-6-4-2-5-3-1
两种遍历的递归写法等价:

  1. function codec(res = [], root) { // code_1
  2. if(!root) res.push("#");
  3. else{
  4. res.push(root.val);
  5. codec(root.left);
  6. codec(root.right);
  7. }
  8. return res;
  9. }
  10. let res = []; // code_2
  11. function codec_2(root) {
  12. if (!root) res.push("#");
  13. else {
  14. res.push(root.val);
  15. codec(res, root.left);
  16. codec(res, root.right);
  17. }
  18. }

二叉树遍历的迭代写法
层次遍历: [102] 二叉树的层序遍历

  1. var levelOrder = function(root) { // 当前层、下一层
  2. if(!root)return [];
  3. let res = [];
  4. let cur_layer = [root]; // 初始层为根节点
  5. while(cur_layer.length){ // 当前层不为空时
  6. let next_layer = []; // 用于存储下一层结点
  7. let cur_vals = []; // 用于存储当前层的结果
  8. for(let i =0;i<cur_layer.length;i++){ // 遍历当前层的结点
  9. cur_vals.push(cur_layer[i].val); // 将当前层结果存储起来
  10. if(cur_layer[i].left){ // 若该节点左子树存在,则存入下一层
  11. next_layer.push(cur_layer[i].left);
  12. }
  13. if(cur_layer[i].right){ // 若该节点右子树存在,则存入下一层
  14. next_layer.push(cur_layer[i].right);
  15. }
  16. }
  17. if(cur_vals.length)res.push(cur_vals);
  18. cur_layer = next_layer; // 当前层遍历完毕, 将当前层替换为下一层
  19. }
  20. return res;
  21. };

结点结构

  1. // 结点结构1
  2. function TreeNode(val, left, right) {
  3. this.val = (val===undefined ? 0 : val)
  4. this.left = (left===undefined ? null : left)
  5. this.right = (right===undefined ? null : right)
  6. }
  7. // 结点结构2
  8. class TreeNode {
  9. constructor(val) {
  10. this.val = val;
  11. this.left = null;
  12. this.right = null;
  13. }
  14. }

二叉搜索树(BST)

定义:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。
二叉树算法设计的总路线:把当前节点要做的事做好,其他的交给递归框架,不用当前节点操心。
特点:中序遍历得到有序数列。

BST遍历框架

  1. function BST(root,target){
  2. if(target === root.val){
  3. // do something...
  4. }
  5. else if(target > root.val){
  6. BST(root.right, target)
  7. }
  8. else{
  9. BST(root.left, target)}
  10. }

例题

判断是否BST
可以通过辅助函数增长参数列表,借助参数传递信息。

  1. var isValidBST = function (root) {
  2. // 验证是否是二叉搜索树
  3. function helper(root, min, max) {
  4. if (root === null) return true;
  5. if (root.val <= min || root.val >= max) return false;
  6. return (
  7. helper(root.left, min, root.val) && helper(root.right, root.val, max)
  8. );
  9. }
  10. return helper(root, -Infinity, Infinity);
  11. };

判断是否有目标值

  1. function isInBST(root, target) {
  2. // BST中是否有目标值,有则返回对应根,无则null
  3. if (target === root.val) return root;
  4. if (target > root.val && root.right !== null)
  5. return isInBST(root.right, target);
  6. else if (target < root.val && root.left !== null)
  7. return isInBST(root.left, target);
  8. else return null;
  9. }

插入一个数
从大范围考虑时,左右子树需要变化,则写法1;从小范围考虑时,一直遍历找到需要变化的地方,则写法2。

  1. // 写法1:
  2. var insertIntoBST = function (root, target) {
  3. // 若根节点不存在,则插入新节点
  4. if (root === null) return new TreeNode(target);
  5. if (root.val < target) {
  6. // 目标值插在根节点右边,则右子树需要变化
  7. root.right = insertIntoBST(root.right, target);
  8. } else if (target < root.val) {
  9. // 目标值插在根节点右左边,则左子树需要变化
  10. root.left = insertIntoBST(root.left, target);
  11. }
  12. return root;
  13. };
  14. // 写法2:
  15. var insertIntoBST = function (root, target) {
  16. function helper(root, target) {
  17. if (root === null) return new TreeNode(target);
  18. if (target < root.val && !!root.left) return helper(root.left, target);
  19. else if (target > root.val && !!root.right)
  20. return helper(root.right, target);
  21. else if (target < root.val && root.left === null)
  22. root.left = new TreeNode(target);
  23. else if (target > root.val && root.right === null)
  24. root.right = new TreeNode(target);
  25. return root;
  26. }
  27. if (root === null) return new TreeNode(target);
  28. helper(root, target);
  29. return root;
  30. };

删除一个数

  1. function deleteBSTNode(root, target) {
  2. if (root == null) return null;
  3. if (root.val === target) {
  4. // 找到则删除
  5. if (root.left === null && root.right === null) return null;
  6. if (root.left == null) return root.right;
  7. if (root.right == null) return root.left;
  8. if (root.left != null && root.right != null) {
  9. // 左右子树都不为空,则用左子树最大的节点代替,或者使用右子树最小节点代替,然后删除对应节点
  10. var minNode = minInBST(root.right);
  11. root.val = minNode.val;
  12. root.right = deleteBSTNode(root.right, minNode.val);
  13. }
  14. } else if (root.val < target) {
  15. root.right = deleteBSTNode(root.right, target);
  16. } else if (target < root.val) {
  17. root.left = deleteBSTNode(root.left, target);
  18. }
  19. }

最小、最大的数

  1. function minInBST(root) {
  2. // BST中最小的数
  3. if (root.left) return minInBST(root.left);
  4. return root;
  5. }
  6. function maxBST(root) {
  7. // BST中最大的数
  8. if (root.right) return maxBST(root.right);
  9. else return root;
  10. }

其它树

完全二叉树、满二叉树

散列表(哈希表)

参考资料:

堆可以用完全二叉树来表示。堆可分为两类,分别是最大堆和最小堆。对于最小堆,每个根结点值都小于等于其左右子节点;对于最大堆,每个根结点值都大于等于其左右子节点;这里以最大堆为例。
常常和优先队列

堆的表示

堆一般使用数组来表示:
数据结构 - 图10
如图所示,设根节点下标为x,则左子节点下标=x2,右子节点下标=x2+1。根结点下标=孩子结点下标 / 2。

二叉堆的相关操作

以最大堆为例

  1. // 插入元素、上浮操作(将大的数上浮到顶部)
  2. // 删除最大数、下沉操作(先将最大数[在堆顶]与最后一个元素交换,删除最后一个元素,然后将堆顶元素下沉到合适位置)

优先队列

优先队列包括最大优先队列、最小优先队列。而优先队列的底层数据结构就是堆,最大堆—>最大优先队列,最小堆—>最小优先队列。
image.png
优先队列的ADT:

  1. // --- 优先队列主要操作 ---
  2. insert(key, data) // 元素入队操作,以key为排序依据,data为数据
  3. deleteQueueTop(key) // 删除堆顶元素(最小 / 最大)
  4. getQueueTop(key) // 返回堆顶元素
  5. // --- 优先队列辅助操作 ---
  6. k最小/第k最大:返回优先队列中键值为第k个最小/最大的元素
  7. 大小(size):返回优先队列中的元素个数
  8. 堆排序(Heap Sort):基于键值的优先级将优先队列中的元素进行排序

具体实现:

参考:二叉堆实现优先级队列

参考资料:

图的分类

无向图、有向图、无向权重图、有向权重图
数据结构 - 图12

图的表示方法

  1. 边列表

数据结构 - 图13

  1. 邻接矩阵

数据结构 - 图14

  1. 邻接表

数据结构 - 图15

  1. 逆邻接表

数据结构 - 图16

图的实现

扩展

LRU算法

数据结构的构造为:哈希表+双向链表