算法总结归纳(二)

注:题目来源力扣https://leetcode-cn.com/

算法总结归纳(一)

5、链表专题

        链表(Linked List) 是一种常见的基础数据结构, 是一种线性表, 但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer) 。

        由于不必须按顺序存储, 链表在插入的时候可以达到 O(1)O(1) 的复杂度, 比另一种线性表—— 顺序表快得多, 但是查找一个节点或者访问特定编号的节点则需要 O(n)O(n) 的时间, 而顺序表相应的时间复杂度分别是 O(log\ n)O(log n) 和 O(1)O(1)。

        使用链表结构可以克服数组链表需要预先知道数据大小的缺点, 链表结构可以充分利用计算机内存空间, 实现灵活的内存动态管理。 但是链表失去了数组随机读取的优点, 同时链表由于增加了结点的指针域, 空间开销比较大。

        链表允许插入和移除表上任意位置上的节点, 但是不允许随机存取。 链表有很多种不同的类型: 单向链表, 双向链表以及循环链表。

5.1 合并两个有序链表

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

示例1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例2:

输入:l1 = [], l2 = []
输出:[]

示例3:

输入:l1 = [], l2 = [0]
输出:[0]

解题思路:

首先,设定一个虚拟节点 dummy 用来存储结果,循环对比 L1 和 L2 节点上的数字,通过调整 p节点的 next 指针来调整 dummy 的结果。

  • 如果 L1 当前位置的值小于等于 L2 ,我们就把  L1 的值接在  dummy 节点的后面同时将  L1 指针往后移一个

  • 如果 L2 当前位置的值小于 L2 ,我们就把  L2 的值接在  p 节点的后面同时将  L2 指针往后移一个

  • 不管我们将哪一个元素接在了 p 节点后面,都需要向后移一个元素

  • 重复以上过程,直到  L1 或者  L2 指向了 null 

  • 在循环终止的时候,  L1 和  L2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表。


/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode* dummy = new ListNode(0);
     ListNode* p = dummy;
 
    while (l1 != nullptr && l2 != nullptr) {
      if (l1->val < l2->val) {
        p->next = l1;
        l1 = l1->next;
      } else {
        p->next = l2;
        l2 = l2->next;
      }
      p = p->next;
    }
 
    if (l1 != nullptr) p->next = l1;
    if (l2 != nullptr) p->next = l2;
    return dummy->next;
    }
}

5.2 复杂链表的复制

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

 

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

 

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

 解题思路:

本题要求我们对一个特殊的链表进行深拷贝。如果是普通链表,我们可以直接按照遍历的顺序创建链表节点。而本题中因为随机指针的存在,当我们拷贝节点时,「当前节点的随机指针指向的节点」可能还没创建,因此我们需要变换思路。一个可行方案是,我们利用回溯的方式,让每个节点的拷贝操作相互独立。对于当前节点,我们首先要进行拷贝,然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝,拷贝完成后将创建的新节点的指针返回,即可完成当前节点的两指针的赋值。

具体地,我们用哈希表记录每一个节点对应新节点的创建情况。遍历该链表的过程中,我们检查「当前节点的后继节点」和「当前节点的随机指针指向的节点」的创建情况。如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过,如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。

在实际代码中,我们需要特别判断给定节点为空节点的情况。

class Solution {
public:
    unordered_map<Node*, Node*> cachedNode;

    Node* copyRandomList(Node* head) {
        if (head == nullptr) {
            return nullptr;
        }
        if (!cachedNode.count(head)) {
            Node* headNew = new Node(head->val);
            cachedNode[head] = headNew;
            headNew->next = copyRandomList(head->next);
            headNew->random = copyRandomList(head->random);
        }
        return cachedNode[head];
    }
};

5.3 排序链表

        给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

输入:head = [4,2,1,3]
输出:[1,2,3,4]

 

 输入:head = [-1,5,3,4,0]

输出:[-1,0,3,4,5]

解题思路:

使用自底向上的方法实现归并排序,则可以达到 O(1) 的空间复杂度。

首先求得链表的长度length,然后将链表拆分成子链表进行合并。

具体做法如下。

1、用 subLength 表示每次需要排序的子链表的长度,初始时 subLength=1。

2、每次将链表拆分成若干个长度为subLength 的子链表(最后一个子链表的长度可以小于 subLength),按照每两个子链表一组进行合并,合并后即可得到若干个长度为subLength×2 的有序子链表(最后一个子链表的长度可以小于subLength×2)。

3、将 subLength 的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length,整个链表排序完毕。

如何保证每次合并之后得到的子链表都是有序的呢?可以通过数学归纳法证明。

初始时 subLength=1,每个长度为 11 的子链表都是有序的。

如果每个长度为 subLength 的子链表已经有序,合并两个长度为 subLength 的有序子链表,得到长度为subLength×2 的子链表,一定也是有序的。

当最后一个子链表的长度小于 subLength 时,该子链表也是有序的,合并两个有序子链表之后得到的子链表一定也是有序的。

因此可以保证最后得到的链表是有序的。

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        int length = 0;
        ListNode* node = head;
        while (node != nullptr) {
            length++;
            node = node->next;
        }
        ListNode* dummyHead = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode* prev = dummyHead, *curr = dummyHead->next;
            while (curr != nullptr) {
                ListNode* head1 = curr;
                for (int i = 1; i < subLength && curr->next != nullptr; i++) {
                    curr = curr->next;
                }
                ListNode* head2 = curr->next;
                curr->next = nullptr;
                curr = head2;
                for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
                    curr = curr->next;
                }
                ListNode* next = nullptr;
                if (curr != nullptr) {
                    next = curr->next;
                    curr->next = nullptr;
                }
                ListNode* merged = merge(head1, head2);
                prev->next = merged;
                while (prev->next != nullptr) {
                    prev = prev->next;
                }
                curr = next;
            }
        }
        return dummyHead->next;
    }

    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead, *temp1 = head1, *temp2 = head2;
        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;
        }
        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }
        return dummyHead->next;
    }
};

5.4 奇偶链表

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。

第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。

请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。

你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。

 解题思路:

如果链表为空,则直接返回链表。

对于原始链表,每个节点都是奇数节点或偶数节点。头节点是奇数节点,头节点的后一个节点是偶数节点,相邻节点的奇偶性不同。因此可以将奇数节点和偶数节点分离成奇数链表和偶数链表,然后将偶数链表连接在奇数链表之后,合并后的链表即为结果链表。

原始链表的头节点 head 也是奇数链表的头节点以及结果链表的头节点,head 的后一个节点是偶数链表的头节点。令 evenHead = head.next,则 evenHead 是偶数链表的头节点。

维护两个指针 odd 和 even 分别指向奇数节点和偶数节点,初始时 odd = head,even = evenHead。通过迭代的方式将奇数节点和偶数节点分离成两个链表,每一步首先更新奇数节点,然后更新偶数节点。

更新奇数节点时,奇数节点的后一个节点需要指向偶数节点的后一个节点,因此令 odd.next = even.next,然后令 odd = odd.next,此时 odd 变成 even 的后一个节点。

更新偶数节点时,偶数节点的后一个节点需要指向奇数节点的后一个节点,因此令 even.next = odd.next,然后令 even = even.next,此时 even 变成 odd 的后一个节点。

在上述操作之后,即完成了对一个奇数节点和一个偶数节点的分离。重复上述操作,直到全部节点分离完毕。全部节点分离完毕的条件是 even 为空节点或者 even.next 为空节点,此时 odd 指向最后一个奇数节点(即奇数链表的最后一个节点)。

最后令 odd.next = evenHead,将偶数链表连接在奇数链表之后,即完成了奇数链表和偶数链表的合并,结果链表的头节点仍然是 head。

class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        ListNode* evenHead = head->next;
        ListNode* odd = head;
        ListNode* even = evenHead;
        while (even != nullptr && even->next != nullptr) {
            odd->next = even->next;
            odd = odd->next;
            even->next = odd->next;
            even = even->next;
        }
        odd->next = evenHead;
        return head;
    }
};

6、广度优先搜索专题

广度优先搜索(宽度优先搜索)算法是最简便的图的搜索算法之一, 这一算法也是很多重要的图的算法的原型。 Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和广度优先搜索类似的思想。其别名又叫BFS, 属于一种盲目搜寻法, 目的是系统地展开并检查图中的所有节点, 以找寻结果。 换句话说, 它并不考虑结果的可能位置, 彻底地搜索整张图, 直到找到结果为止。

6.1 二叉树的层次遍历 II

        给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)。

 解题思路:

        树的层次遍历可以使用广度优先搜索实现。从根节点开始搜索,每次遍历同一层的全部节点,使用一个列表存储该层的节点值。

        如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可:在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。

        为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)O(1)。 C++  中,我们需要返回一个 vector 或 list,它不方便在头部插入元素(会增加时间开销),所以我们可以先用尾部插入的方法得到从上到下的层次遍历列表,然后再进行反转。

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        auto levelOrder = vector<vector<int>>();
        if (!root) {
            return levelOrder;
        }
        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty()) {
            auto level = vector<int>();
            int size = q.size();
            for (int i = 0; i < size; ++i) {
                auto node = q.front();
                q.pop();
                level.push_back(node->val);
                if (node->left) {
                    q.push(node->left);
                }
                if (node->right) {
                    q.push(node->right);
                }
            }
            levelOrder.push_back(level);
        }
        reverse(levelOrder.begin(), levelOrder.end());
        return levelOrder;
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是二叉树中的节点个数。每个节点访问一次,结果列表使用链表的结构时,在结果列表头部添加一层节点值的列表的时间复杂度是O(1),因此总时间复杂度是 O(n)。

空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n。

6.2 跳跃游戏 III

这里有一个非负整数数组 arr,你最开始位于该数组的起始下标 start 处。当你位于下标 i 处时,你可以跳到 i + arr[i] 或者 i - arr[i]。

请你判断自己是否能够跳到对应元素值为 0 的 任一 下标处。

示例1:
输入:arr = [4,2,3,0,3,1,2], start = 5
输出:true
解释:
到达值为 0 的下标 3 有以下可能方案: 
下标 5 -> 下标 4 -> 下标 1 -> 下标 3 
下标 5 -> 下标 6 -> 下标 4 -> 下标 1 -> 下标 3 

示例2:
输入:arr = [4,2,3,0,3,1,2], start = 0
输出:true 
解释:
到达值为 0 的下标 3 有以下可能方案: 
下标 0 -> 下标 4 -> 下标 1 -> 下标 3

示例3:
输入:arr = [3,0,2,1,2], start = 2
输出:false
解释:无法到达值为 0 的下标 1 处。 

解题思路: 

我们可以使用广度优先搜索的方法得到从 start 开始能够到达的所有位置,如果其中某个位置对应的元素值为 0,那么就返回 True。

具体地,我们初始时将 start 加入队列。在每一次的搜索过程中,我们取出队首的节点 u,它可以到达的位置为 u + arr[u] 和 u - arr[u]。如果某个位置落在数组的下标范围 [0, len(arr)) 内,并且没有被搜索过,则将该位置加入队尾。只要我们搜索到一个对应元素值为 0 的位置,我们就返回 True。在搜索结束后,如果仍然没有找到符合要求的位置,我们就返回 False。

class Solution {
public:
    bool canReach(vector<int>& arr, int start) {
        if (arr[start] == 0) {
            return true;
        }
        
        int n = arr.size();
        vector<bool> used(n);
        queue<int> q;
        q.push(start);
        used[start] = true;

        while (!q.empty()) {
            int u = q.front();
            q.pop();
            if (u + arr[u] < n && !used[u + arr[u]]) {
                if (arr[u + arr[u]] == 0) {
                    return true;
                }
                q.push(u + arr[u]);
                used[u + arr[u]] = true;
            }
            if (u - arr[u] >= 0 && !used[u - arr[u]]) {
                if (arr[u - arr[u]] == 0) {
                    return true;
                }
                q.push(u - arr[u]);
                used[u - arr[u]] = true;
            }
        }
        return false;
    }
};

复杂度分析:

  • 时间复杂度:O(N)O(N),其中 NN 是数组 arr 的长度。

  • 空间复杂度:O(N)O(N)。

6.3 从上到下打印二叉树

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

解题思路:


        题目要求的二叉树的 从上至下 打印(即按层打印),又称为二叉树的 广度优先搜索(BFS)。
BFS 通常借助 队列 的先入先出特性来实现。

特例处理: 当树的根节点为空,则直接返回空列表 [] ;
初始化: 打印结果列表 res = [] ,包含根节点的队列 queue = [root] ;
BFS 循环: 当队列 queue 为空时跳出;
大小size:计算队列的大小也就是当前根结点有多少孩子结点
当前层打印循环: 循环次数为当前层节点数size(即队列 queue 长度);
出队: 队首元素出队,记为 node;
打印: 将 node.val 添加至列表 tmp 尾部;
添加子节点: 若 node 的左(右)子节点不为空,则将左(右)子节点加入队列 queue ;
返回值: 返回打印结果列表 res 即可。

class Solution {
public:
    vector<int> levelOrder(TreeNode* root) {
        /* 根据函数返回值定义存储结果的变量 */
        vector<int> result;
        /* 定义一个队列用于存储节点的数据 */
        queue<TreeNode*> que;
        /* 判空 */
        if(root != NULL) que.push(root);

        /* 开始层序遍历 */
        while(!que.empty()) {
            /* 计算队列的大小也即有多少个孩子 */
            int size = que.size();
            /* 层序遍历 */
            for(int i = 0; i < size; i++) {
                /* 获取第一个节点数据 */
                TreeNode* node = que.front();
                que.pop();

                result.push_back(node->val);
                if(node->left != NULL) que.push(node->left);
                if(node->right != NULL) que.push(node->right);

            }
        }
        return result;
    }
};

6.4 扫雷游戏

给你一个大小为 m x n 二维字符矩阵 board ,表示扫雷游戏的盘面,其中:

1. 'M' 代表一个 未挖出的 地雷,
2. 'E' 代表一个 未挖出的 空方块,
3. 'B' 代表没有相邻(上,下,左,右,和所有4个对角线)地雷的 已挖出的 空白方块,
4. 数字('1' 到 '8')表示有多少地雷与这块 已挖出的 方块相邻,
5. 'X' 则表示一个 已挖出的地雷。
        给你一个整数数组 click ,其中 click = [clickr, clickc] 表示在所有 未挖出的 方块('M' 或者 'E')中的下一个点击位置(clickr 是行下标,clickc 是列下标)。

根据以下规则,返回相应位置被点击后对应的盘面:

1. 如果一个地雷('M')被挖出,游戏就结束了- 把它改为 'X' 。
2. 如果一个 没有相邻地雷 的空方块('E')被挖出,修改它为('B'),并且所有和其相邻的未挖出          方块都应该被递归地揭露。
3. 如果一个 至少与一个地雷相邻 的空方块('E')被挖出,修改它为数字('1' 到 '8' ),表示相邻         地雷的数量。
4. 如果在此次点击中,若无更多方块可被揭露,则返回盘面。

 解题思路:

        由于题目要求你根据规则来展示执行一次点击操作后游戏面板的变化,所以我们只要明确该扫雷游戏的规则,并用代码模拟出来即可。

那我们着眼于题目的规则,会发现总共分两种情况:

1. 当前点击的是「未挖出的地雷」,我们将其值改为 X 即可。
2. 当前点击的是「未挖出的空方块」,我们需要统计它周围相邻的方块里地雷的数量cnt(即 M 的数量)。如果 cnt 为零,即执行规则 2,此时需要将其改为B,且递归地处理周围的八个未挖出的方块,递归终止条件即为规则 4,没有更多方块可被揭露的时候。否则执行规则 3,将其修改为数字即可。
        整体看来,一次点击过程会从一个位置出发,逐渐向外圈扩散,所以这引导我们利用「搜索」的方式来实现。这里以广度优先搜索为例:我们定义递归函数 dfs(x, y) 表示当前在(x,y) 点,执行扫雷规则的情况,我们只要按照上面理出来的情况来进行模拟即可,在cnt 为零的时候,对当前点相邻的未挖出的方块调用递归函数,否则将其改为数字,结束递归。

class Solution {
public:
    int dir_x[8] = {0, 1, 0, -1, 1, 1, -1, -1};
    int dir_y[8] = {1, 0, -1, 0, 1, -1, 1, -1};

    void bfs(vector<vector<char>>& board, int sx, int sy) {
        queue<pair<int, int>> Q;
        vector<vector<int>> vis(board.size(), vector<int>(board[0].size(), 0));
        Q.push({sx, sy});
        vis[sx][sy] = true;
        while (!Q.empty()) {
            auto pos = Q.front();
            Q.pop();
            int cnt = 0, x = pos.first, y = pos.second;
            for (int i = 0; i < 8; ++i) {
                int tx = x + dir_x[i];
                int ty = y + dir_y[i];
                if (tx < 0 || tx >= board.size() || ty < 0 || ty >= board[0].size()) {
                    continue;
                }
                // 不用判断 M,因为如果有 M 的话游戏已经结束了
                cnt += board[tx][ty] == 'M';
            }
            if (cnt > 0) {
                // 规则 3
                board[x][y] = cnt + '0';
            } else {
                // 规则 2
                board[x][y] = 'B';
                for (int i = 0; i < 8; ++i) {
                    int tx = x + dir_x[i];
                    int ty = y + dir_y[i];
                    // 这里不需要在存在 B 的时候继续扩展,因为 B 之前被点击的时候已经被扩展过了
                    if (tx < 0 || tx >= board.size() || ty < 0 || ty >= board[0].size() || board[tx][ty] != 'E' || vis[tx][ty]) {
                        continue;
                    }
                    Q.push(make_pair(tx, ty));
                    vis[tx][ty] = true;
                }
            }
        }
    }

    vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
        int x = click[0], y = click[1];
        if (board[x][y] == 'M') {
            // 规则 1
            board[x][y] = 'X';
        } else {
            bfs(board, x, y);
        }
        return board;
    }
};

复杂度分析:

时间复杂度:O(nm)O(nm),其中 nn 和 mm 分别代表面板的宽和高。最坏情况下会遍历整个面板。
空间复杂度:O(nm)O(nm)。我们需要 O(nm)O(nm) 的标记数组来标记当前位置是否已经被加入队列防止重复计算。

6.5 找树左下角的值

        给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。

解题思路:

看到题目有几个关键词:1. 最底层, 2. 最左边
很明显通过层序遍历找到最后一层然后返回第一个值即可,那么问题也就拆分为两个,也就是两关键词

1.如何找到最后一层:使用一个队列,每一次循环队列中所有的子节点加入到队列中,然后将循环队列中的子节点pop掉,最后一次循环也就是没有子节点加入的一次循环即为最后一层
2.如何记录每层最左边的值:使用一个全局变量,每次循环时先把第一个队列元素拿出来做记录,其他的不做处理,这样子每次记录的就是这一层的最左边的元素,然后循环跑完记录的也就是最后一层的元素

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        // 构建队列
        queue<TreeNode*> q;
        // 把根节点放进队列中,这也就是第一层啦
        q.push(root);
        // 维护答案变量,记录每一层最左边的元素
        int ans;
        while (!q.empty()) {
            // 取出这一层第一个元素,即最左边的元素
            TreeNode* tmp = q.front();
            q.pop();
            // 更新ans的值
            ans = tmp->val;
            // 在把最左边的元素的子节点加入到队列中前记录一下队列长度,这个长度也就是这一层还有多少个节点
            int cnt = q.size();
            // 把最左节点的子节点加入到队列中
            if (tmp->left) q.push(tmp->left);
            if (tmp->right) q.push(tmp->right);
            for (int i = 0; i < cnt; ++i) {
                tmp = q.front();
                q.pop();
                // 把这一层的其他节点的子节点加入队列
                if (tmp->left) q.push(tmp->left);
                if (tmp->right) q.push(tmp->right);
            }
        }
        // 返回答案
        return ans;
    }
};

7、深度优先搜索专题

        深度优先搜索算法(英语: Depth-First-Search, DFS) 是一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点, 尽可能深的搜索树的分支。 当节点v的所在边都己被探寻过, 搜索将回溯到发现节点v 的那条边的起始节点。 这一过程一直进行到已发现从源节点可达的所有节点为止。 如果还存在未被发现的节点, 则选择其中一个作为源节点并重复以上过程, 整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。
        深度优先搜索是图论中的经典算法, 利用深度优先搜索算法可以产生目标图的相应拓扑排序表, 利用拓扑排序表可以方便的解决很多相关的图论问题, 如最大路径问题等等。

7.1 检查平衡性

        实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个节点,其两棵子树的高度差不超过 1。

 解题思路:

1.根据平衡性的定义, 计算每个节点的左右子树高度。
2. 如果某个节点的左右子树高度差大于1, 则不平衡, 返回false, 算法结束; 否则说明以当前节点为根的子树是平衡的, 继续检查当前节点的左右子树的平衡性。

class Solution {
public:
    int height(TreeNode* root) {
        if (root == NULL) {
            return 0;
        } else {
            return max(height(root->left), height(root->right)) + 1;
        }
    }

    bool isBalanced(TreeNode* root) {
        if (root == NULL) {
            return true;
        } else {
            return abs(height(root->left) - height(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
        }
    }
};

复杂度分析:

时间复杂度:O(n ^ 2),其中 n 是二叉树中的节点个数。
最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 O(n)。
对于节点 p,如果它的高度是 d,则 height(p) 最多会被调用 d 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 h 满足 O(h)=O(logn),因为 d≤h,所以总时间复杂度为 O(nlogn)。对于最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为 O(n^2)。

空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。

7.2 求根到叶子节点数字之和

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。

叶节点 是指没有子节点的节点。

 

 解题思路:

        深度优先搜索是很直观的做法。从根节点开始,遍历每个节点,如果遇到叶子节点,则将叶子节点对应的数字加到数字之和。如果当前节点不是叶子节点,则计算其子节点对应的数字,然后对子节点递归遍历。

class Solution {
public:
    int dfs(TreeNode* root, int prevSum) {
        if (root == nullptr) {
            return 0;
        }
        int sum = prevSum * 10 + root->val;
        if (root->left == nullptr && root->right == nullptr) {
            return sum;
        } else {
            return dfs(root->left, sum) + dfs(root->right, sum);
        }
    }
    int sumNumbers(TreeNode* root) {
        return dfs(root, 0);
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是二叉树的节点个数。对每个节点访问一次。

空间复杂度:O(n),其中 n是二叉树的节点个数。空间复杂度主要取决于递归调用的栈空间,递归栈的深度等于二叉树的高度,最坏情况下,二叉树的高度等于节点个数,空间复杂度为 O(n)。

7.3 二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

 解题思路:

        在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。

        如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。
        如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。
        如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。当然,深度优先搜索也可以使用非递归的方式实现。

class Solution {
public:
    void construct_paths(TreeNode* root, string path, vector<string>& paths) {
        if (root != nullptr) {
            path += to_string(root->val);
            if (root->left == nullptr && root->right == nullptr) {  // 当前节点是叶子节点
                paths.push_back(path);                              // 把路径加入到答案中
            } else {
                path += "->";  // 当前节点不是叶子节点,继续递归遍历
                construct_paths(root->left, path, paths);
                construct_paths(root->right, path, paths);
            }
        }
    }

    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> paths;
        construct_paths(root, "", paths);
        return paths;
    }
};

7.4 路径总和 II

        给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。(叶子节点 是指没有子节点的节点。)

 

解题思路:

我们可以采用深度优先搜索的方式,枚举每一条从根节点到叶子节点的路径。当我们遍历到叶子节点,且此时路径和恰为目标和时,我们就找到了一条满足条件的路径。

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;

    void dfs(TreeNode* root, int targetSum) {
        if (root == nullptr) {
            return;
        }
        path.emplace_back(root->val);
        targetSum -= root->val;
        if (root->left == nullptr && root->right == nullptr && targetSum == 0) {
            ret.emplace_back(path);
        }
        dfs(root->left, targetSum);
        dfs(root->right, targetSum);
        path.pop_back();
    }

    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        dfs(root, targetSum);
        return ret;
    }
};

复杂度分析

时间复杂度:O(N^2),其中 N是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。

空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值