【算法题】链表系列之从尾到头打印链表、重建二叉树、用两个栈实现队列

一、从尾到头打印链表

1.1、题目描述

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

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

1.2、递归法

利用递归,先走至链表末端,回溯时依次将节点值加入列表 ,这样就可以实现链表值的倒序输出。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void dfs(ListNode* head,vector<int> &nums)
    {
        if(head->next==NULL)
        {
            nums.emplace_back(head->val);
            return;
        }
        
        dfs(head->next,nums);
        nums.emplace_back(head->val);
    }
    vector<int> reversePrint(ListNode* head) {
        vector<int> nums;
        if(head==NULL)
            return nums;
        dfs(head,nums);
        return nums;

    }
};

时间复杂度O(N): 遍历链表,递归 N次。
空间复杂度O(N): 系统递归需要使用 O(N)的栈空间。

1.3、栈(stack)

栈的特点是后进先出,即最后压入栈的元素最先弹出。利用栈的这一特点,使用栈将链表元素顺序倒置。从链表的头节点开始,依次将每个节点压入栈内,然后依次弹出栈内的元素并存储到数组中。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        vector<int> nums;
        if(head==NULL)
            return nums;
        stack<int> st;
        ListNode* cur=head;
        while(cur)
        {
            st.push(cur->val);
            cur=cur->next;
        }

        while(!st.empty())
        {
            nums.emplace_back(st.top());
            st.pop();
        }
        return nums;
    }
};

时间复杂度O(N)。
空间复杂度O(N)。

二、重建二叉树

2.1、题目描述

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

示例 1:

Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]

在这里插入图片描述

示例 2:

Input: preorder = [-1], inorder = [-1]
Output: [-1]

来源:力扣(LeetCode)

2.2、前置知识:

二叉树前序遍历的顺序为:

  1. 先遍历根节点;
  2. 随后递归地遍历左子树;
  3. 最后递归地遍历右子树。

二叉树中序遍历的顺序为:

  1. 先递归地遍历左子树;
  2. 随后遍历根节点;
  3. 最后递归地遍历右子树。

对于任意一颗树而言,前序遍历的形式总是:

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。

中序遍历的形式总是:

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]

2.3、分治算法

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。

(1)算法思路:

  1. 通过【前序遍历列表】确定【根节点 (root)】;
  2. 将【中序遍历列表】的节点分割成【左分支节点】和【右分支节点】;
  3. 递归寻找【左分支节点】中的【根节点 (left child)】和 【右分支节点】中的【根节点 (right child)】。

(2)实现逻辑:

  1. 先保存前序遍历列表到全局变量中,方便使用。
  2. 创建一个字典(比如C++的unordered_map容器),将中序遍历列表的元素和下标保存下来;方便计算左右子树在前序遍历列表中的开始位置和长度。
  3. 递归函数实现,参数是根节点在前序遍历列表的下标、左子树开始节点在前序遍历列表的下标、右子树结束节点在前序遍历列表的下标;递归的终止条件是左子树的开始下标大于右子树的结束下标(即 left>right)。
  4. 递归函数要创建根节点,前序遍历列表的开始位置即是根节点的值,然后依次递归左子树和右子树。
  5. 字典保存了中序遍历列表的下标,可以利用它来计算左右子树的长度。

在这里插入图片描述

代码实现:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
private:
    unordered_map<int,int> map;// 存储中序遍历数组的元素和下标,用于确定左右子树的元素数量/长度。
    vector<int> preorder;     // 保存中序遍历数组,方便递归时依据索引查看先序遍历的值
    TreeNode* myBuildTree(int root,int left,int right)
    {
        if(left>right)
            return nullptr;
        // 创建根节点
        TreeNode* rtn=new TreeNode(preorder[root]);
        // 划分根节点、左子树、右子树
        int idx=map[preorder[root]];
        // 递归左子树
        rtn->left=myBuildTree(root+1,left,idx-1);
        // 递归右子树
        rtn->right=myBuildTree(root+idx-left+1,idx+1,right);
        // 回溯返回根节点
        return rtn;
    }
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n=preorder.size();
        if(n==0)
            return nullptr;
        int i=0;
        this->preorder=preorder;
        for(i=0;i<n;i++)
            map[inorder[i]]=i;
            
        return myBuildTree(0,0,n-1);
    }
};

执行用时:8 ms, 在所有 C++ 提交中击败了95.43%的用户
内存消耗:24.8 MB, 在所有 C++ 提交中击败了63.43%的用户

时间复杂度 O(N) 。

空间复杂度 O(N) 。

2.4、小结

利用前序遍历和中序遍历的特性,重建二叉树;根据分治法实现递归运算。

三、用两个栈实现队列

3.1、题目描述

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )。

示例 1:

输入:
[“CQueue”,“appendTail”,“deleteHead”,“deleteHead”,“deleteHead”]
[[],[3],[],[],[]]
输出:[null,null,3,-1,-1]

示例 2:

输入:
[“CQueue”,“deleteHead”,“appendTail”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

来源:力扣(LeetCode)

3.2、双栈法

将一个栈当作输入栈,用于压入 appendTail 传入的数据;另一个栈当作输出栈,用于 deleteHead 操作。

每次deleteHead 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。

代码实现:

class CQueue {
private:
    stack<int> st;// 输入栈
    stack<int> st2;//输出栈
public:
    CQueue() {
        
    }
    
    void appendTail(int value) {
        st.push(value);
    }
    
    int deleteHead() {
        
        if(st2.empty())
        {
            if(st.empty())
                return -1;
            while(!st.empty())
            {
                st2.push(st.top());
                st.pop();
            }
        }

        int ret=st2.top();
        st2.pop();

        return ret;
    }
};

时间复杂度:O(1)。

空间复杂度:O(n)。

3.3、小结

双栈法。

  1. 一个栈作为入栈、一个栈作为出栈;
  2. 当出栈为空时,再将入栈所有数据弹出,压到输出栈;
  3. 如果两个栈都为空,则返回-1。

总结

一定要做好总结,特别是当没有解出题来,没有思路的时候,一定要通过结束阶段的总结来反思犯了什么错误。解出来了也一定要总结题目的特点,题目中哪些要素是解出该题的关键。不做总结的话,花掉的时间所得到的收获通常只有 50% 左右。

在题目完成后,要特别注意总结此题最后是归纳到哪种类型中,它在这种类型中的独特之处是什么。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值