前端怎么能不懂数据结构呢

数据结构在学校的教材、网上的课程中大部分都是以 C、C++语言为例子介绍,今天我们就用 JavaScript 重新介绍一下基础数据结构:栈、队列、链表、树。
每种数据结构都会简单介绍一下其概念,并使用 JavaScript 实现,同时附上 leetcode 题目加深理解

概念

  • 遵从后进先出原则的有续集合
  • 添加新元素的一端称为栈顶,另一端称为栈底
  • 操作栈的元素时,只能从栈顶操作,如:添加元素、移除或取值

实现

需要实现的几个基本功能如下

  • push:入栈方法
  • pop: 出栈方法
  • top:获取栈顶值
  • size: 获取栈的元素个数
  • clear: 清空栈
js 复制代码
class Stack {
  constructor() {
    this.data = {};
    this.count = 0;
  }
  push(item) {
    this.data[this.count++] = item;
  }
  pop() {
    if (!this.size()) {
      return;
    }
    const top = this.data[this.count - 1];
    delete this.data[--this.count];
    return top;
  }
  top() {
    if (!this.size()) {
      return;
    }
    return this.data[this.count - 1];
  }
  size() {
    return this.count;
  }
  clear() {
    this.data = {};
    this.count = 0;
  }
}

栈的最小值

面试题 03.02. 栈的最小值

题目

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

示例:

js 复制代码
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();   --> 返回 0.
minStack.min();   --> 返回 -2.

提示:各函数的调用总次数不超过 20000 次

题解

js 复制代码
var MinStack = function () {
  this.data = {};
  this.count = 0;
  this.minData = {};
  this.minCount = 0;
};

/**
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function (x) {
  this.data[this.count++] = x;
  if (!this.minCount || this.min() >= x) {
    this.minData[this.minCount++] = x;
  }
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function () {
  if (this.top() === this.min()) {
    delete this.minData[--this.minCount];
  }
  delete this.data[--this.count];
};

/**
 * @return {number}
 */
MinStack.prototype.top = function () {
  return this.data[this.count - 1];
};

/**
 * @return {number}
 */
MinStack.prototype.min = function () {
  return this.minData[this.minCount - 1];
};

每日温度

739. 每日温度

题目

请根据每日 气温 列表 temperatures ,重新生成一个列表,要求其对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

输入: temperatures = [73,74,75,71,69,72,76,73] 输出: [1,1,4,2,1,1,0,0]

示例 2:

输入: temperatures = [30,40,50,60] 输出: [1,1,1,0]

示例 3:

输入: temperatures = [30,60,90] 输出: [1,1,0]

提示:

  • 1 <= temperatures.length <= 105
  • 30 <= temperatures[i] <= 100

题解

常规方法:遍历,双层循环

单调栈:

  • 因为要找升温的,所以我们这里反着来,规定栈是递减的
  • 一开始栈是空的,第一个元素入栈;
  • 第二个元素与栈顶对比,如果比栈顶值大,说明找到了升温的一天,当前的索引减去栈顶的索引就是需要等待的时间;同时栈顶值出栈,因为已经找到栈顶值升温等待的天数了,无需再处理它了;
  • 这时候如果栈中还有值,继续拿当前值去与栈顶值比较,如果还是大于栈顶值,就重复上一步,如果小于栈顶值,就将当前值入栈,循环取下一个值来继续比较
js 复制代码
/**
 * @param {number[]} temperatures
 * @return {number[]}
 */
var dailyTemperatures = function (temperatures) {
  const stack = [temperatures[0]];
  const len = temperatures.length;
  const arr = new Array(len).fill(0);
  for (let i = 0; i < len; i++) {
    const curtemp = temperatures[i];
    while (stack.length && curtemp > stack[stack.length - 1]) {
      // 也可以将 stack 改成存索引,这里就不用再 findIndex 了
      const stackIndex = temperatures.findIndex(
        (cur) => cur === stack[stack.length - 1]
      );
      arr[stackIndex] = i - stackIndex;
      stack.pop();
    }
    stack.push(curtemp);
  }
  return arr;
};

队列

概念

  • 遵循先进先出的有序集合
  • 添加新元素的一端为队尾,另一端为队首

实现

基于数组实现

js 复制代码
class Queue {
  constructor() {
    this.queue = [];
    this.count = 0;
  }
  // 入队
  enQueue(item) {
    this.queue[this.count++] = item;
  }
  // 出队
  deQueue() {
    if (this.isEmpty()) {
      return;
    }
    this.count--;
    return this.queue.shift();
  }
  // 是否为空
  isEmpty() {
    return this.count === 0;
  }
  // 队首元素
  top() {
    if (this.isEmpty()) {
      return;
    }
    return this.queue[0];
  }
  length() {
    return this.count;
  }
  clear() {
    this.count = 0;
    this.queue = [];
  }
}

基于对象实现

js 复制代码
class Queue {
  constructor() {
    this.queue = {};
    this.count = 0;
    this.head = 0;
  }
  // 入队
  enQueue(item) {
    this.queue[this.count++] = item;
  }
  // 出队
  deQueue() {
    if (this.isEmpty()) {
      return;
    }
    const headData = this.queue[this.head];
    delete this.queue[this.head];
    this.head++;
    return headData;
  }
  length() {
    return this.count - this.head;
  }
  // 是否为空
  isEmpty() {
    return this.length() === 0;
  }
  clear() {
    this.queue = [];
    this.count = 0;
    this.head = 0;
  }
}

双端队列

双端队列(double-ended queue)指的是允许同时从队尾与队首两端进行存取操作的队列,操作更加灵活

基本功能如下:

  • addFront:从头添加元素
  • addBack:从尾部添加元素
  • removeFront:删除头部元素
  • removeBack:删除末尾元素
  • frontTop:获取头部元素
  • backTop:获取末尾元素
js 复制代码
class Deque {
  constructor() {
    this.queue = {};
    this.count = 0;
    this.head = 0;
  }
  addFront(item) {
    this.queue[--this.head] = item;
  }
  addBack(item) {
    this.queue[this.count++] = item;
  }
  removeFront() {
    if (this.isEmpty()) {
      return;
    }
    const front = this.queue[this.head];
    delete this.queue[this.head++];
    return front;
  }
  removeBack() {
    if (this.isEmpty()) {
      return;
    }
    const back = this.queue[this.count - 1];
    delete this.queue[--this.count];
    return back;
  }
  frontTop() {
    if (this.isEmpty()) {
      return;
    }
    return this.queue[this.head];
  }
  backTop() {
    if (this.isEmpty()) {
      return;
    }
    return this.queue[this.count - 1];
  }
  length() {
    return this.count - this.head;
  }
  isEmpty() {
    return this.length() === 0;
  }
}

队列的最大值

题目

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数 max_value、push_back 和 pop_front 的均摊时间复杂度都是 O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

less 复制代码
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

示例 2:

less 复制代码
输入:
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

限制:

  • 1 <= push_back,pop_front,max_value 的总操作数 <= 10000
  • 1 <= value <= 10^5

题解

js 复制代码
var MaxQueue = function () {
  this.queue = {};
  this.count = 0;
  this.head = 0;

  this.dque = {};
  this.dcount = 0;
  this.dhead = 0;
};

/**
 * @return {number}
 */
MaxQueue.prototype.max_value = function () {
  return this.dque[this.dhead];
};

/**
 * @param {number} value
 * @return {void}
 */
MaxQueue.prototype.push_back = function (value) {
  this.queue[this.count++] = value;
  if (!this.dque[this.dhead]) {
    this.dque[this.dhead] = value;
  } else if (value > this.dque[this.dhead]) {
    this.dque[this.dhead--] = value;
  }
};

/**
 * @return {number}
 */
MaxQueue.prototype.pop_front = function () {
  if (this.count === this.head) {
    return -1;
  }
  const front = this.queue[this.head];
  delete this.queue[this.head++];
  return front;
};

滑动窗口的最大值

239. 滑动窗口最大值

题目

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7] 解释:

css 复制代码
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

提示:

你可以假设 k 总是有效的,在输入数组 不为空 的情况下,1 ≤ k ≤ nums.length

题解

js 复制代码
/**
 * @param {number[]} nums 传入数组
 * @param {number} k 滑动窗口宽度
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
  if (k <= 1) {
    return nums;
  }
  const result = [];
  const deque = [];
  // 1、将窗口第一个位置的数据添加到 deque 中,保持递减
  deque.push(nums[0]);
  let i = 1;
  for (; i < k; i++) {
    // deque存在数据
    // 当前数据大于队尾值
    // 出队,再重复比较
    while (deque.length && nums[i] > deuqe[deque.length - 1]) {
      deque.pop();
    }
    deque.push(nums[i]);
  }
  // 将第一个位置的最大值添加到result
  result.push(deque[0]);

  // 2、遍历后续数据
  for (; i < nums.length; i++) {
    while (deque.length && nums[i] > deuqe[deque.length - 1]) {
      deque.pop();
    }
    deque.push(nums[i]);
    // 检测当前最大值是否位于窗口外
    if (deque[0] === nums[i - k]) {
      deque.shift();
    }
    // 添加最大值到result
    result.push(deque[0]);
  }
};

链表

链表是有序的数据结构

与栈、队列的区别就是:链表可以从首、尾、中间任一位置进行数据存取,灵活度更高一些

到这里你可能就会有疑问了,这直接用数组不就完了?

为什么不直接用数组?

原因很简单,在做某些操作时,链表的性能优于数组

数组是用来存有序数据的一种数据类型,在内存中需要占用一段连续的空间;也因为它占用的空间是连续的,所以我们可以直接使用索引快速找到对应的元素;当我们对数组进行添加或者移除操作时,就可能出现性能开销比较大的情况,例如在非末尾的位置添加、移除元素,就会导致后续的元素出现位移。

直接用数据说话,这是两段操作数组的代码,

js 复制代码
const arr = [];
for (let i = 0; i < 100000; i++) {
  arr.push(i);
}
js 复制代码
const arr = [];
for (let i = 0; i < 100000; i++) {
  arr.unshift(i);
}

借助 jsbench 工具,得到两段代码的执行效率如下:

  • push 操作:1112 ops/s
  • unshift 操作:2.9 ops/s

(JSBench 的使用欢迎看我这一篇文章 ☞ js 性能优化(实战篇))

概念

  • 链表是有序的数据结构
  • 链表中的每个部分称为节点
  • 可以从首、尾、中间进行数据存取
  • 链表的元素在内存中不必是连续的空间

优势

添加与删除操作不会导致其他元素的位移,主要使用场景是一些经常需要添加、删除元素的数据

劣势

无法根据索引快速定位元素

实现

因为链表在内存中并不需要是连续的,所以我们这里使用 next 标记链表中的下一个元素

这里我们实现两个类:

  • 节点类:value、next
  • 链表类
    • addAtTail 尾部添加节点
    • addAtHead 头部添加节点
    • addAtIndex 指定位置添加节点
    • get 读取节点
    • removeAtIndex 删除指定位置的节点
ts 复制代码
// 节点类
class LinkedNode {
  value: unknown;
  next: null | LinkedNode;
  constructor(val: unknown) {
    this.value = val;
    this.next = null;
  }
}
ts 复制代码
// 链表类
class LinkedList {
  count: number;
  head: null | LinkedNode;
  constructor() {
    // 节点个数
    this.count = 0;
    // 头部"指针"
    this.head = null;
  }
  // 尾部添加节点
  addAtTail(val: unknown) {
    const node = new LinkedNode(val);
    if (!this.count || !this.head) {
      // 链表为空
      this.head = node;
      this.count++;
      return;
    }
    // 链表有数据
    let cur = this.head;
    // 循环查找尾部节点
    while (cur.next) {
      cur = cur.next;
    }
    // 将新节点添加到末尾
    cur.next = node;
    this.count++;
  }
  // 头部添加节点
  addAtHead(val: unknown) {
    const node = new LinkedNode(val);
    if (this.count) {
      // 链表不为空
      node.next = this.head;
    }
    this.head = node;
    this.count++;
  }
  // 指定位置添加节点
  addAtIndex(index: number, val: unknown) {
    const node = new LinkedNode(val);
    const target = this.get(index);
    if (!target) {
      return false;
    }
    node.next = target;
    const prev = this.get(index - 1);
    if (prev) {
      prev.next = node;
    }
    return true;
  }
  // 根据索引获取节点
  get(index: number) {
    if (!this.count || index < 0 || index >= this.count || !this.head) {
      /**
       * 异常数据处理
       * 1. 链表为空
       * 2. index 为负数
       * 3. index 超出链表范围
       * 4. head 为 null
       */
      return null;
    }
    // 从首部开始向后获取节点
    let target = this.head;
    let count = 0;
    while (target.next && count < index) {
      /**
       * 存在下一个节点
       * 且还未到达指定位置
       */
      target = target.next;
      count++;
    }
    if (index === count) {
      return target;
    }
    return null;
  }
  // 删除指定位置的节点
  removeAtIndex(index: number) {
    const target = this.get(index);
    if (!target) {
      return false;
    }
    const prev = this.get(index - 1);
    if (prev) {
      prev.next = target.next;
    }
    return true;
  }
}

链表类型

双向链表

指在普通链表的基础上,增加一个用于记录上一个节点的属性 prev,可进行双向访问。

首节点的 prev 指向空,末尾节点的 next 指向空。

循环链表

又称 "环形链表",指的是链表最后一个节点的 next 指向第一个节点,形成首尾相连的循环结构。

实际使用中,最后一个节点的 next 不一定要指向第一个节点,可能是链表中任一位置的节点。

反转链表

LeetCode 206.反转链表

题目

反转一个单链表。

示例:

rust 复制代码
输入:1->2->3->4->5->NULL
输出:5->4->3->2->1->NULL

进阶:你可以迭代或递归地反转链表。你能否用两种方式解决这道题?

题解

js 复制代码
/**
 * Definition for singly-llinked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null :: next)
 * }
 */

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head) {
  let cur = head;
  let prev = null;
  while (cur) {
    const next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
  }
  return prev;
};

递归解法

js 复制代码
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function (head) {
  if (!head || !head.next) {
    return head;
  }
  const newHead = reverseList(head.next);
  // 第一次执行这里的节点是倒数第二个节点
  head.next.next = head;
  // head 的 next 在下一次递归时会被重新设置
  // 最后一次的 next 会被设置为 null
  head.next = null;
  return newHead;
};

环路检测

面试题 02.08. 环路检测

题目

给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。若环不存在,请返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

示例 1:

输入:head = [3,2,0,-4], pos = 1 输出:tail connects to node index 1 解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0 输出:tail connects to node index 0 解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1 输出:no cycle 解释:链表中没有环。

进阶:

你是否可以不用额外空间解决此题?

题解:哈希表

js 复制代码
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function (head) {
  const visited = new Set();
  while (head !== null) {
    if (visited.has(head)) {
      return head;
    }
    visited.add(head);
    head = head.next;
  }
  return null;
};

题解:快慢指针

  • slow 指针,每次移动一位
  • fast 指针,每次移动两位

fast 移动的距离是 slow 的两倍,因此如果链表中存在环,那么 fast 与 slow 必定会相遇

js 复制代码
var detectCycle = function (head) {
  let slow = head;
  let fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (!fast) {
      return null;
    }
    if (slow === fast) {
      // 存在环路
      let ptr = head;
      /**
       * 新指针 ptr 从 head 开始移动
       * slow 继续移动
       * ptr 与 slow 相遇的位置即为环的起点
       */
      while (ptr.next) {
        if (ptr === slow) {
          return ptr;
        }
        ptr = ptr.next;
        slow = slow.next;
      }
    }
  }
  return null;
};

树与二叉树

树的概念

栈、队列都属于线性结构,而树属于非线性结构

树中每个部分被称为结点,节点之间存在分支结构与层次关系

每个树形结构都具有一个根结点,如下图中的 A 结点

根据节点之间的关系,还存在 父结点子结点兄弟结点

不含子结点的结点称为 叶结点,如上图中的 D、E、F

以树的角度,还存在 子树 的概念,指的是对某个结点与其后代结点的统称

B、D、H 就可以看作是子树

由于树存在父子关系,树的结点形成了多级结构,每一层被称为 层级

从根结点开始,根结点的层级为 1,向下依次递增

最深结点的层级被称为树的 高度,上图中树的高度就是 3

二叉树的概念

树结构中的一种,二叉树的每个结点最多只能存在两个子结点

二叉树中子结点有更明确的称呼,左子结点右子结点

还有一些特殊形式的二叉树,如:满二叉树完全二叉树

  • 满二叉树:每层结点都达到最大值
  • 完全二叉树:除了最后一层外,每层结点都达到最大值,且最后一层结点都位于左侧

二叉树的存储形式

主要有两种:顺序存储方式链式存储方式

顺序存储方式主要是完全二叉树和满二叉树使用,因为它的结构是连续的,可以按照从左到右、从上到下的顺序将结点存储在数组中;

对于普通二叉树也可以使用顺序存储,不存在的结点可以使用 null 等空值代替,但是对存储空间是一种浪费;

因此,普通二叉树通常采用链式存储方式,记录结点间的关系,每个结点通过 value 表示值,leftright 表示左右子结点

二叉树的遍历

根据访问顺序的不同,有三种不同的遍历方式:前序遍历中序遍历后序遍历

  • 前序遍历:根结点 -> 左子树 -> 右子树
  • 中序遍历:左子树 -> 根结点 -> 右子树
  • 后序遍历:左子树 -> 右子树 -> 根结点

二叉树的前序遍历

144. 二叉树的前序遍历

题目

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例 1:

输入:root = [1,null,2,3] 输出:[1,2,3]

示例 2:

输入:root = [] 输出:[]

示例 3:

输入:root = [1] 输出:[1]

示例 4:

输入:root = [1,2] 输出:[1,2]

示例 5:

输入:root = [1,null,2] 输出:[1,2]

提示:

  • 树中节点数目在范围 [0, 100]
  • -100 <= Node.val <= 100

进阶:递归算法很简单,你可以通过迭代算法完成吗?

题解:递归方式

js 复制代码
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function (root) {
  if (!root) {
    return [];
  }
  return [
    root.val,
    ...preorderTraversal(root.left),
    ...preorderTraversal(root.right),
  ];
};

题解:迭代方式

js 复制代码
var preorderTraversal = function (root) {
  const result = [];
  const stack = [];
  while (root || stack.length) {
    while (root) {
      stack.push(root.right);
      result.push(root.val);
      root = root.left;
    }
    root = stack.pop();
  }
  return result;
};

二叉树的最大深度

104. 二叉树的最大深度

题目

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

示例 1:

输入:root = [3,9,20,null,null,15,7] 输出:3

示例 2:

输入:root = [1,null,2] 输出:2

提示:

  • 树中节点的数量在 [0, 104] 区间内。
  • -100 <= Node.val <= 100

题解:深度优先搜索(DFS)

js 复制代码
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
  if (!root) {
    return 0;
  }
  return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
};

二叉树的层序遍历

102. 二叉树的层序遍历

题目

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]

示例 2:

输入:root = [1] 输出:[[1]]

示例 3:

输入:root = [] 输出:[]

提示:

  • 树中节点数目在范围 [0, 2000]
  • -1000 <= Node.val <= 1000

题解:广度优先搜索(BFS)

js 复制代码
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
  const result = [];
  if (!root) {
    return result;
  }
  // 声明队列,缓存一下同层的结点
  const q = [root];
  while (q.length) {
    result.push([]);
    const len = q.length;
    // 遍历队列,读取同层结点
    for (let i = 0; i < len; i++) {
      const node = q.shift();
      result[result.length - 1].push(node.val);
      // 如果还存在左右子结点,存入队列中
      if (node.left) {
        q.push(node.left);
      }
      if (node.right) {
        q.push(node.right);
      }
    }
  }
  return result;
};

二叉搜索树

一种特殊的二叉树形式,简称 BST

左子树的结点小于根结点,右子树的结点大于根结点

子树也是二叉搜索树

验证二叉搜索树

98. 验证二叉搜索树

题目

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入:root = [2,1,3] 输出:true

示例 2:

输入:root = [5,1,4,null,null,3,6] 输出:false 解释:根节点的值是 5 ,但是右子节点的值是 4 。

提示:

  • 树中节点数目范围在 [1, 104]
  • -231 <= Node.val <= 231 - 1

题解:递归

js 复制代码
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isValidBST = function (root) {
  return helper(root, -Infinity, Infinity);
};

const helper = (root, lower, upper) => {
  if (!root) {
    return true;
  }
  if (root.val >= upper || root.val <= lower) {
    return false;
  }
  return (
    helper(root.left, lower, root.val) && helper(root.right, root.val, upper)
  );
};

题解:中序遍历

以上图的二叉搜索树为例,如果是用中序遍历,遍历的结果如下:

[3, 7, 9, 10, 12, 15]

可以明显看出,得到的结果就是递增数组

先来个中序遍历算法

js 复制代码
var inorderTraversal = function (root) {
  const stack = [];
  const result = [];
  while (root || stack.length) {
    while (root) {
      // 先将根结点入栈
      stack.push(root);
      // 移动到左子结点,继续入栈
      root = root.left;
    }
    /**
     * 走到这
     * stack 中缓存了左子结点和根结点
     * 3、7、10
     */
    root = stack.pop();
    result.push(root.val);
    // 移动到右结点
    root = root.right;
  }
  return result;
};

通过中序遍历得到了 result,接下来只要判断一下 result 是不是递增

js 复制代码
var isValidBST = function (root) {
  const stack = [];
  const result = [];
  while (root || stack.length) {
    while (root) {
      stack.push(root);
      root = root.left;
    }
    root = stack.pop();
    if (result[result.length - 1] >= root.val) {
      /**
       * 判断下一个结点是否大于上一个结点
       * 为 false 就代表 result 不是递增的
       */
      return false;
    }
    result.push(root.val);
    root = root.right;
  }
  return true;
};

我们看到这个时候 result 就显的有点多余了,我们只需要对比上一个结点而已,不需要记录所有结点的值,再改造一下

js 复制代码
var isValidBST = function (root) {
  const stack = [];
  let last = -Infinity;
  while (root || stack.length) {
    while (root) {
      stack.push(root);
      root = root.left;
    }
    root = stack.pop();
    if (last >= root.val) {
      return false;
    }
    last = root.val;
    root = root.right;
  }
  return true;
};

以上,就是前端视角下的基础数据结构;今年来行业不景气,趁此机会好好充实一下自己吧

相关推荐
算法歌者21 分钟前
[算法]入门1.矩阵转置
算法
喵叔哟28 分钟前
重构代码之取消临时字段
java·前端·重构
林开落L36 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色37 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download38 分钟前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
SoraLuna1 小时前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷1 小时前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿1 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
九圣残炎1 小时前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts