一. 二叉树

二叉树的性质:

1.树是 非线性数据结构

存储 “一对多”的数据结构。

2.度:

image.png
3.层次从1开始。深度是层次的最大值。
image.png

3.完全二叉树

完全二叉树 不是 完美二叉树。
image.png

4.堆是完全二叉树

可以使用 完全二叉树的性质。

5.普通二叉树 性质

image.png

6.完全二叉树的性质:计算 叶子节点的个数

一般二叉树:
n = n0 + n1 + n2;
n - 1 = n1 + 2 n2; // -1 是去掉跟节点
=> n0 = n2 + 1; // n0 和 n2 相差很少
*完全二叉树

1)n0 = ?
n0 = n2 + 1; // n0 和 n2 相差很少
由 n = n0 + n1 + n2;
=> n = n0 + n1 + (n0 - 1);
=> n0 = ((n + 1) - n1) / 2;
n 为奇数,n0 = (n + 1) / 2;
n 为偶数,n0 = n / 2;
2)n1 为 0, n 为奇数
3)n1 为 1,n 为偶数

n 为 3, n0 为 2
n 为 4, n0 为 2
n 为 5, n0 为 3
n 为 6, n0 为 3
=> n0 = Math.ceil(n / 2);

n 为 3, 非 n0 为 1
n 为 4, 非 n0 为 2
n 为 5, 非 n0 为 2
n 为 6, 非 n0 为 3
=> n1 + n2 = Math.floor(n / 2);

二叉搜索树:http://caibaojian.com/js-bst.html

二. 堆

堆 是 完全二叉树,在构建堆时,应用到了 完全二叉树的性质,比如说 非叶子结点的下标是
[0, Math.floor(n / 2)]。
另外构建堆,分为两步:(参考:https://zhuanlan.zhihu.com/p/89600623
1)从末尾非叶子几点遍历
2)以每个非叶子节点为根节点递归

1.构建最大堆

  1. function createHeap(arr) {
  2. let n = arr.length;
  3. const maxHeapify = function(rootIndex) {
  4. let leftIndex = 2 * rootIndex + 1;
  5. let rightIndex = 2 * rootIndex + 2;
  6. let largest = rootIndex; // 流动红旗
  7. // 左右子树先后竞争
  8. if (leftIndex < n && arr[leftIndex] > arr[largest]) {
  9. largest = leftIndex;
  10. }
  11. if (rightIndex < n && arr[rightIndex] > arr[largest]) {
  12. largest = rightIndex;
  13. }
  14. // 检查竞争结果,察觉红旗是否最终改动到子树上
  15. if (largest !== rootIndex) {
  16. // 交换
  17. let tmp = arr[rootIndex];
  18. arr[rootIndex] = arr[largest];
  19. arr[largest] = tmp;
  20. // 递归
  21. maxHeapify(largest);
  22. }
  23. };
  24. // 遍历 非叶子节点
  25. for (let i = Math.floor(n / 2); i >= 0; i--) {
  26. maxHeapify(i);
  27. }
  28. }

2.最大堆排序

  1. function createHeap(arr, heapSize) { // 添加heapsize参数:在不复制数组的情况下,对原数组精确操作
  2. let n = heapSize;
  3. const maxHeapify = function(rootIndex) {
  4. let leftIndex = 2 * rootIndex + 1;
  5. let rightIndex = 2 * rootIndex + 2;
  6. let largest = rootIndex; // 流动红旗
  7. // 左右子树先后竞争
  8. if (leftIndex < n && arr[leftIndex] > arr[largest]) {
  9. largest = leftIndex;
  10. }
  11. if (rightIndex < n && arr[rightIndex] > arr[largest]) {
  12. largest = rightIndex;
  13. }
  14. // 检查竞争结果,察觉红旗是否最终改动到子树上
  15. if (largest !== rootIndex) {
  16. // 交换
  17. let tmp = arr[rootIndex];
  18. arr[rootIndex] = arr[largest];
  19. arr[largest] = tmp;
  20. // 递归
  21. maxHeapify(largest);
  22. }
  23. };
  24. for (let i = Math.floor(n / 2); i >= 0; i--) {
  25. maxHeapify(i);
  26. }
  27. }
  28. console.log(createHeap([3, 4, 5, 1, 5, 7, 2]));
  29. function swap(arr, i, j) {
  30. let tmp = arr[i];
  31. arr[i] = arr[j];
  32. arr[j] = tmp;
  33. }
  34. // function heapSort(arr) {
  35. // let n = arr.length;
  36. // for (let i = 0; i < n - 1; i++) {
  37. // createHeap(arr.slice(0, n - i)); // 避免陷入怎么传入数组的困惑,也避免传入起始下标,因为开始下标是固定的,只需要传入堆的长度即可
  38. // swap(arr, 0, n - 1 - i);
  39. // }
  40. // return arr;
  41. // }
  42. function heapSort(arr) {
  43. let n = arr.length;
  44. for (let i = 0; i < n - 1; i++) {
  45. createHeap(arr, n - i); // 避免陷入怎么传入数组的困惑,也避免传入起始下标,因为开始下标是固定的,只需要传入堆的长度即可
  46. swap(arr, 0, n - i - 1);
  47. }
  48. return arr;
  49. }
  50. console.log(heapSort([3, 4, 5, 1, 5, 7, 2]));

3.最大堆 插入/删除 新值

堆 在这里 是以数组存储。
插入:在已经生成的最大堆插入新值,是将 新值 放到数组末尾,然后重复生成最大堆的动作。
删除,是将数组末尾的数 覆盖 root 之后,然后重复生成最大堆的动作。
图例

  1. function deleteHeap(arr) {
  2. let n = arr.length;
  3. arr[0] = arr[n - 1];
  4. arr.length = n - 1; // 缩减数组长度
  5. createHeap(arr, arr.length);
  6. }
  7. function addHeap(arrm, num) {
  8. arr.push(num);
  9. createHeap(arr, arr.length);
  10. }

相关题目:

3.最小堆

很好改:

  1. function createHeap(arr, heapSize) {
  2. let n = heapSize;
  3. const minHeapify = function(rootIndex) {
  4. let leftIndex = 2 * rootIndex + 1;
  5. let rightIndex = 2 * rootIndex + 2;
  6. let samllest = rootIndex; // 流动红旗 ---> new
  7. // 左右子树先后竞争
  8. if (leftIndex < n && arr[leftIndex] < arr[samllest]) { // ---> new
  9. samllest = leftIndex;
  10. }
  11. if (rightIndex < n && arr[rightIndex] < arr[samllest]) { // ---> new
  12. samllest = rightIndex;
  13. }
  14. // 检查竞争结果,察觉红旗是否最终改动到子树上
  15. if (samllest !== rootIndex) {
  16. // 交换
  17. let tmp = arr[rootIndex];
  18. arr[rootIndex] = arr[samllest];
  19. arr[samllest] = tmp;
  20. // 递归
  21. minHeapify(samllest);
  22. }
  23. };
  24. for (let i = Math.floor(n / 2); i >= 0; i--) {
  25. minHeapify(i);
  26. }
  27. }

这样的排序,最终出来是升序。

4.最大堆在react里的使用

react 算法中的 堆,这个在讲 自己 看多react 源码时,最好 也能手写出来。
以下是 react 的实现:

  1. export function push(heap: Heap, node: Node): void {
  2. const index = heap.length;
  3. heap.push(node);
  4. siftUp(heap, node, index);
  5. }
  6. export function peek(heap: Heap): Node | null {
  7. const first = heap[0];
  8. return first === undefined ? null : first;
  9. }
  10. export function pop(heap: Heap): Node | null {
  11. const first = heap[0];
  12. if (first !== undefined) {
  13. const last = heap.pop();
  14. if (last !== first) {
  15. heap[0] = last;
  16. siftDown(heap, last, 0);
  17. }
  18. return first;
  19. } else {
  20. return null;
  21. }
  22. }
  23. function siftUp(heap, node, i) {
  24. let index = i;
  25. while (true) {
  26. const parentIndex = (index - 1) >>> 1;
  27. const parent = heap[parentIndex];
  28. if (parent !== undefined && compare(parent, node) > 0) {
  29. // The parent is larger. Swap positions.
  30. heap[parentIndex] = node;
  31. heap[index] = parent;
  32. index = parentIndex;
  33. } else {
  34. // The parent is smaller. Exit.
  35. return;
  36. }
  37. }
  38. }
  39. function siftDown(heap, node, i) {
  40. let index = i;
  41. const length = heap.length;
  42. while (index < length) {
  43. const leftIndex = (index + 1) * 2 - 1;
  44. const left = heap[leftIndex];
  45. const rightIndex = leftIndex + 1;
  46. const right = heap[rightIndex];
  47. // If the left or right node is smaller, swap with the smaller of those.
  48. if (left !== undefined && compare(left, node) < 0) {
  49. if (right !== undefined && compare(right, left) < 0) {
  50. heap[index] = right;
  51. heap[rightIndex] = node;
  52. index = rightIndex;
  53. } else {
  54. heap[index] = left;
  55. heap[leftIndex] = node;
  56. index = leftIndex;
  57. }
  58. } else if (right !== undefined && compare(right, node) < 0) {
  59. heap[index] = right;
  60. heap[rightIndex] = node;
  61. index = rightIndex;
  62. } else {
  63. // Neither child is smaller. Exit.
  64. return;
  65. }
  66. }
  67. }
  68. function compare(a, b) {
  69. // Compare sort index first, then task id.
  70. const diff = a.sortIndex - b.sortIndex;
  71. return diff !== 0 ? diff : a.id - b.id;
  72. }

我的实现:(简洁一些,不考虑细枝末节)

  1. // 不用递归,用 while 循环
  2. function push(heap = [], v) {
  3. heap.push(v);
  4. shiftUp(heap, v);
  5. }
  6. function shiftUp(heap, v) {
  7. let index = heap.length;
  8. while (index >= 0) {
  9. let parentIndex = index >>> 1;
  10. let parent = heap[parentIndex];
  11. if (compare(parent, v) > 0) {
  12. heap[parentIndex] = v;
  13. heap[index] = parent;
  14. index = parentIndex;
  15. } else {
  16. return;
  17. }
  18. }
  19. }
  20. function siftDown(heap) { // 斩首,用末尾元素替代头颅,然后规整秩序,记住我们不管左右子节点之间的关系
  21. let index = 0;
  22. // while (true) { // 不知道规定什么条件好,先来个 true
  23. while (index < heap.length) {
  24. let node = heap[index];
  25. let leftIndex = 2 * index + 1;
  26. let rightIndex = 2 * leftIndex + 2;
  27. let left = heap[leftIndex];
  28. let right = heap[rightIndex];
  29. if (compare(left, node) < 0) { // 取最小即可
  30. if (compare(left, right) < 0) { // left 最小,只能和 left 换
  31. heap[leftIndex] = node;
  32. heap[index] = left;
  33. index = leftIndex;
  34. } else {
  35. heap[rightIndex] = node;
  36. heap[index] = right;
  37. index = rightIndex;
  38. }
  39. } else if (compare(right, node) < 0) {
  40. heap[rightIndex] = node;
  41. heap[index] = right;
  42. index = rightIndex;
  43. } else {
  44. // 不动
  45. return;
  46. }
  47. }
  48. }
  49. function compare(a, b) {
  50. return a - b;
  51. }

react 里最多的数据结构:链表相关(单链表 双向链表)
判断单链表是否有环、翻转链表、插入、删除。
判断单链表是否有环,也可以扩展到 对象里是否有循环结构,lodash 是如何解决的?

参考:https://juejin.im/post/6844903482432962573

includes 是 严格的等!
image.png

从这篇文章我想明白为什么要用栈的结构:因为一个对象可能有并列的多个环,另外还要考虑值类型不能是基本类型,只能是指向地址的对象。
https://www.jianshu.com/p/a2bb29aa4fce

三. 链表

仍然是考察代码能力,而非算法

1. 链表结构

  1. class ListNode {
  2. constructor(value) {
  3. this.next = null;
  4. this.value = value;
  5. }
  6. }
  7. class List {
  8. constructor(arr) {
  9. this.head = this.tail = null;
  10. this.length = 0;
  11. if (Array.isArray(arr)) {
  12. for (let i of arr) {
  13. let node = new ListNode(i);
  14. this.append(node);
  15. }
  16. }
  17. }
  18. append(node) {
  19. if (this.head) {
  20. this.tail = this.tail.next = node;
  21. } else {
  22. this.head = this.tail = node;
  23. }
  24. this.length++;
  25. }
  26. deleteByValue(targetValue) {
  27. let p = this.head;
  28. let parent = this.head;
  29. let nodeToDelete = null;
  30. while (p) {
  31. let { value } = p;
  32. if (value === targetValue) {
  33. nodeToDelete = p;
  34. break;
  35. } else {
  36. parent = p;
  37. p = p.next;
  38. }
  39. }
  40. if (nodeToDelete) {
  41. parent.next = nodeToDelete.next;
  42. this.length--;
  43. }
  44. }
  45. }
  46. let l = new List([1, 2, 3, 4, 5]);
  47. console.log('创建链表:', l);
  48. l.deleteByValue(3);
  49. console.log('删除值为3的节点', l);

2. 合并两个有序链表

  1. // 合并有序数组(最简单情况,数组长度相同)
  2. function mergeList(list1, list2) {
  3. let p = list1.head;
  4. let q = list2.head;
  5. let newListHead = null;
  6. let current = null;
  7. while (p && q) {
  8. // 1) 获取最小节点赋值 给c, 并更新 p q
  9. let c = null; // 一开始的误区就是少定义了这个变量
  10. if (p.value < q.value) { // p 和 q 两者取最小
  11. c = p;
  12. p = p.next;
  13. } else {
  14. c = q;
  15. q = q.next;
  16. }
  17. // 2)构造新链表的节点关系
  18. if (newListHead) {
  19. current.next = c;
  20. current = current.next; // current 要变化才行!
  21. } else {
  22. newListHead = c;
  23. current = c;
  24. }
  25. }
  26. return newListHead;
  27. }
  28. let l1 = new List([1, 2, 9, 10]);
  29. let l2 = new List([3, 4, 11, 12]);
  30. console.log(`合并l1l2的结果:`, mergeList(l1, l2));
  31. console.log();

3. 逆转单链表

  1. function inverseList(list) {
  2. let o = null;
  3. let p = list.head;
  4. let q = p.next;
  5. while(q) { // 首先要有三个 o p q 指针,另外要同时保证在 每轮循环 移动 o p q 三个指针的同时,并且修正相互指代的关系
  6. p.next = o;
  7. o = p;
  8. p = q;
  9. q = p.next;
  10. p.next = o;
  11. }
  12. return p;
  13. }
  14. console.log(inverseList(new List([1, 2, 3, 4])));

逆转单链表,从来都是最绕的,没有之一:(下面是第二次写,给的写法)

  1. class List {
  2. // ...
  3. invert() {
  4. // 定义三个相邻的指针
  5. let x = null;
  6. let p = this.head
  7. let y = this.head.next;
  8. while (p) { // while循环的条件只要定为,循环体不报错就好
  9. // 逆转的关键
  10. p.next = x;
  11. // 移动三个指针
  12. x = p;
  13. p = y;
  14. y = y && y.next;
  15. }
  16. this.head = x;
  17. return this;
  18. }
  19. // ...
  20. }

四、图

1. 拓扑排序

可用来检测 图里是否有环!(判断是否有环,常规的做法也可以借助数据结构,比如说一个数组,来存放已经被访问过的元素)。
参考:https://blog.csdn.net/weixin_42001089/article/details/84327306
toposort 是 npm 包,提供给你做 图的拓扑排序。
https://hotttao.github.io/2018/11/19/alog/graph_use2/ 来讲了 拓扑排序 在编译器领域的使用。
image.png
拓扑排序的题目
image.png
答案:

  1. 山的那边海的那边有一群小精灵!