前端数据结构和算法

这篇博客详细探讨了前端开发中常见的数据结构和算法问题,包括链表的反转、环形链表检测、链表合并、求链表中间节点、栈和队列的应用以及二叉树的各种遍历和操作。通过递归和广度优先搜索等方法解决实际问题,如合并排序链表、判断回文链表、实现最大堆和优先队列等。

 

链表

反转链表

原地单链表的反转、两个一组反转链表和K个一组反转链表

1. 简单的反转链表

反转一个单链表。【206】

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
let reverseList =  (head) => {
    if (!head) return null
    let pre = null, cur = head
    while (cur) {
        // 关键: 保存下一个节点的值
        let next = cur.next 
        cur.next = pre
        pre = cur
        cur = next 
    }
    return pre
}

2. 区间反转

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。说明: 1 ≤ m ≤ n ≤ 链表长度。【92】

/**
 * @param {ListNode} head
 * @param {number} m
 * @param {number} n
 * @return {ListNode}
 */
var reverseBetween = function(head, m, n) {
    let p = dummyHead = new ListNode()
    let pre, cur, front, end
    p.next = head
    for(let i = 0; i < m - 1; i ++) {
        p = p.next
    }
    front = p
    pre = end = p.next
    cur = pre.next
    for(let i = 0; i < n - m; i++) {
        let next = cur.next
        cur.next = pre
        pre = cur
        cur = next
    }
    front.next = pre
    end.next = cur
    return dummyHead.next
}

 

3. 两个一组翻转链表

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。给定 1->2->3->4, 你应该返回 2->1->4->3.【24】

var swapPairs = function(head) {
    if(head === null || head.next === null) return head
    let node1 = head, node2 = head.next 
    node1.next = swapPairs(node2.next)
    node2.next = node1
    return node2
}

 

4. K个一组反转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。你的算法只能使用常数的额外空间。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。【25】

递归解法:

/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
var reverseKGroup = function(head, k) {
    let pre = null, cur = head
    let p = head
    // 下面的循环用来检查后面的元素是否能组成一组
    for(let i = 0; i < k; i++) {
        if(p == null) return head
        p = p.next
    }
    for(let i = 0; i < k; i++){
        let next = cur.next
        cur.next = pre
        pre = cur
        cur = next
    }
    // pre为本组最后一个节点,cur为下一组的起点
    head.next = reverseKGroup(cur, k)
    return pre
}

 

环形链表:

1. 如何检测链表形成环

var hasCycle = function(head) {
    let dummyHead = new ListNode()
    dummyHead.next = head 
    let fast = slow = dummyHead
    // 零个结点或者一个结点,肯定无环
    if(fast.next === null || fast.next.next === null) return false
    while(fast && fast.next) {
        fast = fast.next.next
        slow = slow.next
        // 两者相遇了
        if(fast === slow) return true
    } 
    return false
}

2. 如何找到环的起点?

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

**说明:**不允许修改给定的链表。

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
    let dummyHead = new ListNode()
    dummyHead.next = head
    let fast = slow = dummyHead
    // 零个结点或者一个结点,肯定无环
    if(fast.next === null || fast.next.next === null)  return null
    while(fast && fast.next) {
        fast = fast.next.next
        slow = slow.next
        // 两者相遇了
        if(fast === slow) {
           let p = dummyHead
           while(p !== slow) {
               p = p.next
               slow = slow.next
           }
           return p
        }
    } 
    return null
}

链表合并

1. 合并两个有序链表

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。【21】

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    const merge = (l1, l2) => {
        if (l1 === null) return l2
        if (l2 === null) return l1
        if (l1.val > l2.val) {
            l2.next = merge(l1, l2.next)
            return l2
        } else {
            l1.next = merge(l1.next, l2)
            return l1
        }
    }
    return merge(l1, l2)
}

 

2. 合并K个有序链表

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。【23】

自上而下递归实现:

/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    var mergeTwoLists = function(l1, l2) {
    const merge = (l1, l2) => {
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        if(l1.val > l2.val) {
            l2.next = merge(l1, l2.next);
            return l2;
        }else {
            l1.next = merge(l1.next, l2);
            return l1;
        }
    }
    return merge(l1, l2);
    };
    const _mergeLists = (lists, start, end) => {
        if(end - start < 0) return null;
        if(end - start == 0)return lists[end];
        let mid = Math.floor(start + (end - start) / 2);
        return mergeTwoList(_mergeLists(lists, start, mid), _mergeLists(lists, mid + 1, end));
    }
    return _mergeLists(lists, 0, lists.length - 1);
};

自下而上实现:

var mergeKLists = function(lists) {
    var mergeTwoLists = function(l1, l2) {
    const merge = (l1, l2) => {
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        if(l1.val > l2.val) {
            l2.next = merge(l1, l2.next);
            return l2;
        }else {
            l1.next = merge(l1.next, l2);
            return l1;
        }
    }
    return merge(l1, l2);
    };
    // 边界情况
    if(!lists || !lists.length) return null;
    // 虚拟头指针集合
    let dummyHeads = [];
    // 初始化虚拟头指针
    for(let i = 0; i < lists.length; i++) {
        let node = new ListNode();
        node.next = lists[i];
        dummyHeads[i] = node;
    }
    // 自底向上进行merge
    for(let size = 1; size < lists.length; size += size){
        for(let i = 0; i + size < lists.length;i += 2 * size) {
            dummyHeads[i].next = mergeTwoLists(dummyHeads[i].next, dummyHeads[i + size].next);
        }
    }
    return dummyHeads[0].next;
};

求链表中间节点

1. 判断回文链表

判断一个单链表是否为回文链表。你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?【234】

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function(head) {
    let reverse = (pre, cur) => {
        if(!cur) return pre
        let next = cur.next
        cur.next = pre
        return reverse(cur, next)
    }
    let dummyHead = slow = fast = new ListNode()
    dummyHead.next = head
    // 找中点, 黄金模板
    while (fast && fast.next) {
        slow = slow.next
        fast = fast.next.next
    }
    let next = slow.next
    slow.next = null
    let newStart = reverse(null, next)
    for(let p = head, newP = newStart; newP != null; p = p.next, newP = newP.next) {
        if(p.val != newP.val) return false
    }
    return true
}

栈和队列

栈&递归

1. 有效括号

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。【20】

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    let stack = []
    for (let i = 0; i < s.length; i++) {
        let ch = s.charAt(i)
        if(ch == '(' || ch == '[' || ch == '{') 
            stack.push(ch)
        if(!stack.length) return false
        if(ch == ')' && stack.pop() !== '(') return false
        if(ch == ']' && stack.pop() !== '[' ) return false
        if(ch == '}' && stack.pop() !== '{') return false
    }
    return stack.length === 0
}

2. 多维数组 flatten

将多维数组转化为一维数组。

/**
 * @constructor
 * @param {NestedInteger[]} nestedList
 * @return {Integer[]}
 */
let flatten = (nestedList) => {
    let result = []
    let fn = function (target, ary) {
        for (let i = 0; i < ary.length; i++) {
            let item = ary[i]
            if (Array.isArray(ary[i])) {
                fn(target, item)
            } else {
                target.push(item)
            }
        }
    }
    fn(result, nestedList)
    return result

reduce方法:

let flatten = (nestedList) =>  nestedList.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur): cur), [])

二叉树层序遍历

1. 普通的层次遍历

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。【102】

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if(!root) return []
    let queue = []
    let res = []
    let level = 0
    queue.push(root)
    while(queue.length) {
        res.push([])
        let size = queue.length
        // 注意一下: size -- 在层次遍历中是一个非常重要的技巧
        while(size --) {
            // 出队
            let front = queue.shift()
            res[level].push(front.val)
            // 入队
            if(front.left) queue.push(front.left)
            if(front.right) queue.push(front.right)
        }        
        level++
    }
    return res
}

2. 二叉树的锯齿形层次遍历

给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。【103】

var zigzagLevelOrder = function(root) {
    if(!root) return []
    let queue = []
    let res = []
    let level = 0
    queue.push(root)
    while(queue.length) {
        res.push([])
        let size = queue.length
        while(size --) {
            // 出队
            let front = queue.shift()
            res[level].push(front.val)
            if(front.left) queue.push(front.left)
            if(front.right) queue.push(front.right)
        }  
        // 仅仅增加下面一行代码即可
        if(level % 2) res[level].reverse()     
        level++
    }
    return res
};

3. 二叉树的右视图

给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。【199】

使用广度优先的思想,即层序遍历:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var rightSideView = function(root) {
    if(!root) return [];
    let queue = [];
    let res = [];
    queue.push(root);
    while(queue.length) {
        res.push(queue[0].val);
        let size = queue.length;
        while(size --) {
            // 一个size的循环就是一层的遍历,在这一层只拿最右边的结点
            let front = queue.shift();
            if(front.right) queue.push(front.right);
            if(front.left) queue.push(front.left);
        }
    }
    return res;
};

无权图BFS遍历

1. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。【279】

var numSquares = function(n) {
    let map = new Map()
    let queue = []
    queue.push([n, 0])
    map.set(n, true)
    while(queue.length) {
        let [num, step] = queue.shift()
        for(let i = 1; ; i++) {
            let nextNum = num - i * i
            if(nextNum < 0) break
            if(nextNum == 0) return step + 1
            // nextNum 未被访问过
            if(!map.get(nextNum)){
                queue.push([nextNum, step + 1])
                // 标记已经访问过
                map.set(nextNum, true)
            }
        }
    }
}

2. 单词接龙

给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

  • 每次转换只能改变一个字母。
  • 转换过程中的中间单词必须是字典中的单词。

说明:

  1. 如果不存在这样的转换序列,返回 0。
  2. 所有单词具有相同的长度。
  3. 所有单词只由小写字母组成。
  4. 字典中不存在重复的单词。
  5. 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

【127】

/**
 * @param {string} beginWord
 * @param {string} endWord
 * @param {string[]} wordList
 * @return {number}
 */
var ladderLength = function(beginWord, endWord, wordList) {
    // 两个单词在图中是否相邻
    const isSimilar = (a, b) => {
        let diff = 0
        for(let i = 0; i < a.length; i++) {
            if(a.charAt(i) !== b.charAt(i)) diff++;
            if(diff > 1) return false; 
        }
        return true;
    }
    let queue = [beginWord];
    let index = wordList.indexOf(beginWord);
    if(index !== -1) wordList.splice(index, 1);
    let res = 2;
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            for(let i = 0; i < wordList.length; i++) {
                if(!isSimilar(front, wordList[i]))continue;
                // 找到了
                if(wordList[i] === endWord) {
                    return res;
                }
                else {
                    queue.push(wordList[i]);
                }
                // wordList[i]已经成功推入,现在不需要了,删除即可
                // 这一步性能优化,相当关键,不然100%超时
                wordList.splice(i, 1);
                i --;
            }
        }
        // 步数 +1
        res += 1;
    }
    return 0;
};

优先队列

1. 实现一个最大堆

// 以最大堆为例来实现一波
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
class MaxHeap {
  constructor(arr = [], compare = null) {
    this.data = arr;
    this.size = arr.length;
    this.compare = compare;
  }
  getSize() {
    return this.size;
  }
  isEmpty() {
    return this.size === 0;
  }
  // 增加元素
  add(value) {
    this.data.push(value);
    this.size++;
    // 增加的时候把添加的元素进行 siftUp
    this._siftUp(this.getSize() - 1);
  }
  // 找到优先级最高的元素
  findMax() {
    if (this.getSize() === 0)
      return;
    return this.data[0];
  }
  // 让优先级最高的元素(即队首元素)出队
  extractMax() {
    // 1.保存队首元素
    let ret = this.findMax();
    // 2.让队首和队尾元素交换位置
    this._swap(0, this.getSize() - 1);
    // 3. 把队尾踢出去,size--
    this.data.pop();
    this.size--;
    // 4. 新的队首 siftDown
    this._siftDown(0);
    return ret;
  }

  toString() {
    console.log(this.data);
  }
  _swap(i, j) {
    [this.data[i], this.data[j]] = [this.data[j], this.data[i]];
  }
  _parent(index) {
    return Math.floor((index - 1) / 2);
  }
  _leftChild(index) {
    return 2 * index + 1;
  }
  _rightChild(index) {
    return 2 * index + 2;
  }
  _siftUp(k) {
    // 上浮操作,只要子元素优先级比父节点大,父子交换位置,一直向上直到根节点
    while (k > 0 && this.compare(this.data[k], this.data[this._parent(k)])) {
      this._swap(k, this._parent(k));
      k = this._parent(k);
    }
  }
  _siftDown(k) {
    // 存在左孩子的时候
    while (this._leftChild(k) < this.size) {
      let j = this._leftChild(k);
      // 存在右孩子而且右孩子比左孩子大
      if (this._rightChild(k) < this.size &&
        this.compare(this.data[this._rightChild(k)], this.data[j])) {
        j++;
      }
      if (this.compare(this.data[k], this.data[j]))
        return;
      // 父节点比子节点小,交换位置
      this._swap(k, j);
      // 继续下沉
      k = j;
    }
  }
}

2. 实现优先队列

class PriorityQueue {
  // max 为优先队列的容量
  constructor(max, compare) {
    this.max = max;
    this.compare = compare;
    this.maxHeap = new MaxHeap([], compare);
  }

  getSize() {
    return this.maxHeap.getSize();
  }

  isEmpty() {
    return this.maxHeap.isEmpty();
  }

  getFront() {
    return this.maxHeap.findMax();
  }

  enqueue(e) {
    // 比当前最高的优先级的还要高,直接不处理
    if (this.getSize() === this.max) {
      if (this.compare(e, this.getFront())) return;
      this.dequeue();
    }
    return this.maxHeap.add(e);
  }

  dequeue() {
    if (this.getSize() === 0) return null;
    return this.maxHeap.extractMax();
  }
}

优先队列应用

1. 前K个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

说明:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。

【347】

var topKFrequent = function(nums, k) {
   let map = {};
   let pq = new PriorityQueue(k, (a, b) => map[a] - map[b] < 0);
   for(let i = 0; i < nums.length; i++) {
       if(!map[nums[i]]) map[nums[i]] = 1;
       else map[nums[i]] = map[[nums[i]]] + 1;
   }
   let arr = Array.from(new Set(nums));
   for(let i = 0; i < arr.length; i++) {
       pq.enqueue(arr[i]);
   }
   return pq.maxHeap.data;
};

2. 合并 K 个排序链表

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。【23】

/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    let dummyHead = p = new ListNode();
    // 定义优先级的函数,重要!
    let pq = new PriorityQueue(lists.length, (a, b) => a.val <= b.val);
    // 将头结点推入优先队列
    for(let i = 0; i < lists.length; i++) 
        if(lists[i]) pq.enqueue(lists[i]);
    // 取出值最小的节点,如果 next 不为空,继续推入队列
    while(pq.getSize()) {
        let min = pq.dequeue();
        p.next = min;
        p = p.next;
        if(min.next) pq.enqueue(min.next);
    }
    return dummyHead.next;
};

双端队列及应用

1. 滑动窗口最大值

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。要求: 时间复杂度应为线性。【239】

var maxSlidingWindow = function(nums, k) {
    // 异常处理
    if(nums.length === 0 || !k) return [];
    let window = [], res = [];
    for(let i = 0; i < nums.length; i++) {
        // 先把滑动窗口之外的踢出
        if(window[0] !== undefined && window[0] <= i - k) window.shift();
        // 保证队首是最大的
        while(nums[window[window.length - 1]] <= nums[i])  window.pop();
        window.push(i);
        if(i >= k - 1) res.push(nums[window[0]]) 
    }
    return res;
};

栈和队列的相互实现

1. 栈实现队列

使用栈实现队列的下列操作:push(x) -- 将一个元素放入队列的尾部。 pop() -- 从队列首部移除元素。 peek() -- 返回队列首部的元素。 empty() -- 返回队列是否为空。【232】

var MyQueue = function() {
    this.stack1 = [];
    this.stack2 = [];
};

MyQueue.prototype.push = function(x) {
    this.stack1.push(x);
};
// 将 stack1 的元素转移到 stack2
MyQueue.prototype.transform = function() {
  while(this.stack1.length) {
    this.stack2.push(this.stack1.pop());
  }
}

MyQueue.prototype.pop = function() {
  if(!this.stack2.length) this.transform();
  return this.stack2.pop();
};

MyQueue.prototype.peek = function() {
    if(!this.stack2.length) this.transform();
    return this.stack2[this.stack2.length - 1];
};

MyQueue.prototype.empty = function() {
    return !this.stack1.length && !this.stack2.length;
};

2. 队列实现栈【225】

var MyStack = function() {
    this.queue1 = [];
    this.queue2 = [];
};
MyStack.prototype.push = function(x) {
    if(!this.queue2.length) this.queue1.push(x);
    else {
        // queue2 已经有值
        this.queue2.push(x);
        // 旧的栈顶移到 queue1 中
        this.queue1.push(this.queue2.shift());
    }

};
MyStack.prototype.transform = function() {
    while(this.queue1.length !== 1) {
        this.queue2.push(this.queue1.shift())
    }
    // queue2 保存了前面的元素
    // 让 queue1 和 queue2 交换
    // 现在queue1 包含前面的元素,queue2 里面就只包含队尾的元素
    let tmp = this.queue1;
    this.queue1 = this.queue2;
    this.queue2 = tmp;
}
MyStack.prototype.pop = function() {
    if(!this.queue2.length) this.transform();
    return this.queue2.shift();
};
MyStack.prototype.top = function() {
    if(!this.queue2.length) this.transform();
    return this.queue2[0];
};
MyStack.prototype.empty = function() {
    return !this.queue1.length && !this.queue2.length;
};

二叉树

二叉树的遍历

1. 前序遍历【144】

递归方法:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
    let arr = [];
    let traverse = (root) => {
      if(root == null) return;
      arr.push(root.val);
      traverse(root.left);
      traverse(root.right); 
    }
    traverse(root);
    return arr;
};

非递归方法:

var preorderTraversal = function(root) {
    if(root == null) return [];
    let stack = [], res = [];
    stack.push(root);
    while(stack.length) {
        let node = stack.pop();
        res.push(node.val);
        // 左孩子后进先出,进行先左后右的深度优先遍历
        if(node.right) stack.push(node.right);
        if(node.left) stack.push(node.left);
    }
    return res;
};

2. 中序遍历【94】

递归方法:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    let arr = [];
    let traverse = (root) => {
      if(root == null) return;
      traverse(root.left);
      arr.push(root.val);
      traverse(root.right); 
    }
    traverse(root);
    return arr;
};

非递归方法:

var inorderTraversal = function(root) {
    if(root == null) return [];
    let stack = [], res = [];
    let p = root;
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack.pop();
        res.push(node.val);
        p = node.right;
    }   
    return res;
};

3. 后序遍历【145】

递归方法:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    let arr = [];
    let traverse = (root) => {
      if(root == null) return;
      traverse(root.left);
      traverse(root.right);
      arr.push(root.val);
    }
    traverse(root);
    return arr
};

非递归方法:

var postorderTraversal = function(root) {
    if(root == null) return [];
    let stack = [], res = [];
    let visited = new Set();
    let p = root;
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack[stack.length - 1];
        // 如果右孩子存在,而且右孩子未被访问
        if(node.right && !visited.has(node.right)) {
            p = node.right;
            visited.add(node.right);
        } else {
            res.push(node.val);
            stack.pop();
        }
    }
    return res;
};

最大/最小深度

1. 最大深度

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。【104】

递归方法:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    // 递归终止条件 
    if(root == null) return 0;
    return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
};

非递归方法:

var maxDepth = function(root) {
    if(root == null) return 0;
    let queue = [root];
    let level = 0;
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            if(front.left) queue.push(front.left);
            if(front.right) queue.push(front.right);
        }
        // level ++ 后的值代表着现在已经处理完了几层节点
        level ++;
    }
    return level;
};

2. 最小深度【111】

递归方法:

var minDepth = function(root) {
    if(root == null) return 0;
    // 左右孩子都不为空才能像刚才那样调用
    if(root.left && root.right)
        return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
    // 右孩子为空了,直接忽略之
    else if(root.left)
        return minDepth(root.left) + 1;
    // 左孩子为空,忽略
    else if(root.right)
        return minDepth(root.right) + 1;
    // 两个孩子都为空,说明到达了叶子节点,返回 1
    else return 1;
};

非递归方法:

var minDepth = function(root) {
    if(root == null) return 0;
    let queue = [root];
    let level = 0;
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            // 找到叶子节点
            if(!front.left && !front.right) return level + 1;
            if(front.left) queue.push(front.left);
            if(front.right) queue.push(front.right);
        }
        // level ++ 后的值代表着现在已经处理完了几层节点
        level ++;
    }
    return level;
};

对称二叉树

给定一个二叉树,检查它是否是镜像对称的。【101】

递归方法:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    let help = (node1, node2) => {
        // 都为空
        if(!node1 && !node2) return true;
        // 一个为空一个不为空,或者两个节点值不相等
        if(!node1 || !node2 || node1.val !== node2.val) return false;
        return help(node1.left, node2.right) && help(node1.right, node2.left);
    }
    if(root == null) return true;
    return help(root.left, root.right);
};

非递归方法:

var isSymmetric = function(root) {
    if(root == null) return true;
    let queue = [root.left, root.right];
    let node1, node2;
    while(queue.length) {
        node1 = queue.shift();
        node2 = queue.shift();
        // 两节点均为空
        if(!node1 && !node2)continue;
        // 一个为空一个不为空,或者两个节点值不相等
        if(!node1 || !node2 || node1.val !== node2.val) return false;
        queue.push(node1.left);
        queue.push(node2.right);
        queue.push(node1.right);
        queue.push(node2.left);
    }
    return true;
};

LCA问题

LCA (Lowest Common Ancestor)即最近公共祖先问题。“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 【236】

祖先节点集合法:

/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    if(root == null || root == p || root == q) return root;
    let set = new Set();
    let map = new WeakMap();
    let queue = [];
    queue.push(root);
    // 层序遍历
    while(queue.length) {
        let size = queue.length;
        while(size --) {
            let front = queue.shift();
            if(front.left) {
                queue.push(front.left);
                // 记录父亲节点
                map.set(front.left, front);
            }
            if(front.right) {
                queue.push(front.right);
                // 记录父亲节点
                map.set(front.right, front);
            }
        }
    }
    // 构造 p 的上层节点集合
    while(p) {
        set.add(p);
        p = map.get(p);
    }
    while(q) {
        // 一旦发现公共节点重合,直接返回
        if(set.has(q))return q;
        q = map.get(q);
    }
};

深度优先遍历法:

var lowestCommonAncestor = function(root, p, q) {
    if (root == null || root == p || root == q) return root;
    let left = lowestCommonAncestor(root.left, p, q);
    let right = lowestCommonAncestor(root.right, p, q);
    if(left == null) return right;
    else if(right == null) return left;
    return root;
};

二叉搜索树的最近公共祖先 【235】

/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    if(root == null || root == p || root == q) return root;
    // root.val 比 p 和 q 都大,找左孩子
    if(root.val > p.val && root.val > q.val) 
        return lowestCommonAncestor(root.left, p, q);
    // root.val 比 p 和 q 都小,找右孩子
    if(root.val < p.val && root.val < q.val) 
        return lowestCommonAncestor(root.right, p, q);
    else 
        return root;
};

非递归方法:

var lowestCommonAncestor = function(root, p, q) {
    let node = root;
    while(node) {
        if(p.val > node.val && q.val > node.val)
            node = node.right;
        else if(p.val < node.val && q.val < node.val) 
            node = node.left;
        else return node;
    }
};

二叉树中的路径问题

1. 二叉树的直径【543】

 

var diameterOfBinaryTree = function(root) {
    let help = (node) => {
        if(node == null) return 0;
        let left = node.left ? help(node.left) + 1: 0;
        let right = node.right ? help(node.right) + 1: 0;
        let cur = left + right;
        if(cur > max) max = cur; 
        // 这个返回的操作相当关键
        return Math.max(left, right);
    }
    let max = 0;
    if(root == null) return 0;
    help(root);
    return max;
};

 

2. 二叉树的所有路径

给定一个二叉树,返回所有从根节点到叶子节点的路径。 【257】

递归方法:

/**
 * @param {TreeNode} root
 * @return {string[]}
 */
var binaryTreePaths = function(root) {
    let path = [];
    let res = [];
    let dfs = (node) => {
        if(node == null) return;
        path.push(node);
        dfs(node.left);
        dfs(node.right);
        if(!node.left && !node.right) 
            res.push(path.map(item => item.val).join('->'));
        // 注意每访问完一个节点记得把它从path中删除,达到回溯效果
        path.pop();
    }
    dfs(root);
    return res;
};

非递归方法:

var binaryTreePaths = function(root) {
    if(root == null) return [];
    let stack = [];
    let p = root;
    let set = new Set();
    res = [];
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
        }
        let node = stack[stack.length - 1];
        // 叶子节点
        if(!node.right && !node.left) {
            res.push(stack.map(item => item.val).join('->'));
        }
        // 右孩子存在,且右孩子未被访问
        if(node.right && !set.has(node.right)) {
            p = node.right;
            set.add(node.right);
        } else {
            stack.pop();
        }
    }
    return res;
};

3. 二叉树的最大路径和 【124】

递归方法:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxPathSum = function(root) {
    let help = (node) => {
        if(node == null) return 0;
        let left = Math.max(help(node.left), 0);
        let right = Math.max(help(node.right), 0);
        let cur = left + node.val + right;
        // 如果发现某一个节点上的路径值比max还大,则更新max
        if(cur > max) max = cur;
        // left 和 right 永远是"一根筋",中间不会有转折
        return Math.max(left, right) + node.val;
    }
    let max = Number.MIN_SAFE_INTEGER;
    help(root);
    return max;
};

二叉搜索树

1. 验证二叉搜索树

二叉搜索树具有如下特征:节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。【98】

中序遍历方法:

/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isValidBST = function(root) {
    let prev = null;
    const help = (node) => {
        if(node == null) return true;
        if(!help(node.left)) return false;
        if(prev !== null && prev >= node.val) return false;
        // 保存当前节点,为下一个节点的遍历做准备
        prev = node.val;
        return help(node.right);
    }
    return help(root);
};

限定上下界进行DFS:

  递归:

var isValidBST = function(root) {
    const help = (node, max, min) => {
        if(node == null) return true;
        if(node.val >= max || node.val <= min) return false;
        // 左孩子更新上界,右孩子更新下界,相当于边界要求越来越苛刻
        return help(node.left, node.val, min)
                && help(node.right, max, node.val);
    }
    return help(root, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);
};

 

2. 将有序数组转换为二叉搜索树

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。【108】

/**
 * @param {number[]} nums
 * @return {TreeNode}
 */
var sortedArrayToBST = function(nums) {
    let help = (start, end) => {
        if(start > end) return null;
        if(start === end) return new TreeNode(nums[start]);
        let mid = Math.floor((start + end) / 2);
        // 找出中点建立节点
        let node = new TreeNode(nums[mid]);
        node.left = help(start, mid - 1);
        node.right = help(mid + 1, end);
        return node;
    }
    return help(0, nums.length - 1);
};

 

3. 二叉树展开为链表

给定一个二叉(搜索)树,原地将它展开为链表。【114】

/**
 * @param {TreeNode} root
 * @return {void} Do not return anything, modify root in-place instead.
 */
var flatten = function(root) {
    if(root == null) return
    flatten(root.left)
    flatten(root.right)
    if(root.left) {
        let p = root.left
        while(p.right) {
            p = p.right
        }
        p.right = root.right
        root.right = root.left
        root.left = null
    }
};

 

4. 不同的二叉搜索树II

给定一个整数 n,生成所有由 1 ... n 为节点所组成的二叉搜索树。【95】

递归方法:

/**
 * @param {number} n
 * @return {TreeNode[]}
 */
var generateTrees = function(n) {
    if(!n) return []
    let help = (start, end) => {
        if(start > end) return [null]
        if(start === end) return [new TreeNode(start)]
        let res = []
        for(let i = start; i <= end; i++) {
            // 左孩子集
            let leftNodes = help(start, i - 1)
            // 右孩子集
            let rightNodes = help(i + 1, end)
            for(let j = 0; j < leftNodes.length; j++) {
                for(let k = 0; k < rightNodes.length; k++) {
                    let root = new TreeNode(i)
                    root.left = leftNodes[j]
                    root.right = rightNodes[k]
                    res.push(root)
                }
            }
        }
        return res
    }
    return help(1, n)
}

非递归方法:

var generateTrees = function(n) {
    let clone = (node, offset) => {
        if(node == null) return null;
        let newnode = new TreeNode(node.val + offset);
        newnode.left = clone(node.left, offset);
        newnode.right = clone(node.right, offset);
        return newnode;
    }
    if(n == 0) return [];
    let dp = [];
    dp[0] = [null];
    // i 是子问题中的节点个数,子问题: [1], [1,2], [1,2,3]...逐步递增,直到[1,2,3...,n]
    for(let i = 1; i <= n; i++) {
        dp[i] = [];
        for(let j = 1; j <= i; j++) {
            // 左子树集
            for(let leftNode of dp[j - 1]) {
                // 右子树集
                for(let rightNode of dp[i - j]) {
                    let node = new TreeNode(j);
                    // 左子树结构共享
                    node.left = leftNode;
                    // 右子树无法共享,但可以借用节点个数相同的树,每个节点增加一个偏移量
                    node.right = clone(rightNode, j);
                    dp[i].push(node);
                }
            }
        }
    }
    return dp[n];
};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值