【算法题】链表系列
一、从尾到头打印链表
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、前置知识:
二叉树前序遍历的顺序为:
- 先遍历根节点;
- 随后递归地遍历左子树;
- 最后递归地遍历右子树。
二叉树中序遍历的顺序为:
- 先递归地遍历左子树;
- 随后遍历根节点;
- 最后递归地遍历右子树。
对于任意一颗树而言,前序遍历的形式总是:
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。
中序遍历的形式总是:
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
2.3、分治算法
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
(1)算法思路:
- 通过【前序遍历列表】确定【根节点 (root)】;
- 将【中序遍历列表】的节点分割成【左分支节点】和【右分支节点】;
- 递归寻找【左分支节点】中的【根节点 (left child)】和 【右分支节点】中的【根节点 (right child)】。
(2)实现逻辑:
- 先保存前序遍历列表到全局变量中,方便使用。
- 创建一个字典(比如C++的unordered_map容器),将中序遍历列表的元素和下标保存下来;方便计算左右子树在前序遍历列表中的开始位置和长度。
- 递归函数实现,参数是根节点在前序遍历列表的下标、左子树开始节点在前序遍历列表的下标、右子树结束节点在前序遍历列表的下标;递归的终止条件是左子树的开始下标大于右子树的结束下标(即 left>right)。
- 递归函数要创建根节点,前序遍历列表的开始位置即是根节点的值,然后依次递归左子树和右子树。
- 字典保存了中序遍历列表的下标,可以利用它来计算左右子树的长度。
代码实现:
/**
* 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。
总结
一定要做好总结,特别是当没有解出题来,没有思路的时候,一定要通过结束阶段的总结来反思犯了什么错误。解出来了也一定要总结题目的特点,题目中哪些要素是解出该题的关键。不做总结的话,花掉的时间所得到的收获通常只有 50% 左右。
在题目完成后,要特别注意总结此题最后是归纳到哪种类型中,它在这种类型中的独特之处是什么。