Leetcode 数据结构练习
# 数组
# 最大子序和
思路:动态规划
1 2 3 4 5 6 7 8 9 10 class Solution { public int maxSubArray (int [] nums) { int pre = 0 ,maxn = nums[0 ]; for (int x:nums){ pre = Math.max(pre+x,x); maxn = Math.max(pre,maxn); } return maxn; } }
# 两数之和
哈希表 降时间复杂度从 o (n) 到 o (1)
创建一个哈希表,对于每一个 x
,我们首先查询哈希表中是否存在 target - x
,然后将 x
插入到哈希表中,即可保证不会让 x
和自己匹配。
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public int [] twoSum(int [] nums, int target) { Map<Integer, Integer> hashtable = new HashMap <Integer, Integer>(); for (int i = 0 ; i < nums.length; ++i) { if (hashtable.containsKey(target - nums[i])) { return new int []{hashtable.get(target - nums[i]), i}; } hashtable.put(nums[i], i); } return new int [0 ]; } }
# 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
exp
一、双指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : void merge (vector<int >& nums1, int m, vector<int >& nums2, int n) { int p1 = 0 , p2 = 0 ; int sorted[m + n]; int cur; while (p1 < m || p2 < n) { if (p1 == m) { cur = nums2[p2++]; } else if (p2 == n) { cur = nums1[p1++]; } else if (nums1[p1] < nums2[p2]) { cur = nums1[p1++]; } else { cur = nums2[p2++]; } sorted[p1 + p2 - 1 ] = cur; } for (int i = 0 ; i != m + n; ++i) { nums1[i] = sorted[i]; } } };
复杂度分析
时间复杂度:O (m+n)
指针移动单调递增,最多移动 m+n 次,因此时间复杂度为 O (m+n)。
空间复杂度:O (m+n)。
需要建立长度为 m+n 的中间数组 sorted。
二、逆向双指针
从后向前遍历,将两者较大的元素放在 nums 数组的后面而不会被覆盖,降低了空间复杂度为 O (1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : void merge (vector<int >& nums1, int m, vector<int >& nums2, int n) { int p1 = m - 1 , p2 = n - 1 ; int tail = m + n - 1 ; int cur; while (p1 >= 0 || p2 >= 0 ) { if (p1 == -1 ) { cur = nums2[p2--]; } else if (p2 == -1 ) { cur = nums1[p1--]; } else if (nums1[p1] > nums2[p2]) { cur = nums1[p1--]; } else { cur = nums2[p2--]; } nums1[tail--] = cur; } } };
# 两个数组的交集
一、哈希表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : vector<int > intersect (vector<int >& nums1, vector<int >& nums2) { if (nums1.size () > nums2.size ()) { return intersect (nums2, nums1); } unordered_map <int , int > m; for (int num : nums1) { ++m[num]; } vector<int > intersection; for (int num : nums2) { if (m.count (num)) { intersection.push_back (num); --m[num]; if (m[num] == 0 ) { m.erase (num); } } } return intersection; } };
时间复杂度:O (m+n), 空间复杂度:O (min (m,n))
二、双指针排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ``` ### 买卖股票的最佳时机 >输入:[7 ,1 ,5 ,3 ,6 ,4 ] >输出:5 >解释:在第 2 天(股票价格 = 1 )的时候买入,在第 5 天(股票价格 = 6 )的时候卖出,最大利润 = 6 -1 = 5 。 > 注意利润不能是 7 -1 = 6 , 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 exp: **动态规划** (来自题解)考虑每次如何获取最大收益,第i天的最大收益通过前i天的最低点就可以算出来。而第i天以前(包括第i天)的最低点和i-1 天的最低点有关,因此动态方程为 dp[i] = min (d[i-1 ],prices[i]) 其中dp[0 ]=prices[0 ],然后动态计算之后的就可以了。 得到了前i天的最低点以后,只需要维护一个max用来保存最大收益就可以了。 时间复杂度为O (n),一次遍历,空间复杂度O(n)的动态规划,代码如下: ```java int max = 0 ; int [] dp = new int [prices.length]; dp[0 ] = prices[0 ]; for (int i = 1 ; i < prices.length; i++) { dp[i] = (dp[i - 1 ] < prices[i]) ? dp[i - 1 ] : prices[i]; max = (prices[i] - dp[i]) > max ? prices[i] - dp[i] : max; } return max;
接着考虑优化空间,仔细观察动态规划的辅助数组,其实每一次只用到了 dp [-1] 这一个空间,因此可以把数组改成单个变量 dp 来存储截止到第 i 天的价格最低点。优化之后的代码就是题解中的方法二。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int maxProfit (vector<int >& prices) { int inf = 1e9 ; int minprice = inf, maxprofit = 0 ; for (int price: prices) { maxprofit = max (maxprofit, price - minprice); minprice = min (price, minprice); } return maxprofit; } };
时间复杂度 O (n), 空间复杂度 O (1)
# 树和二叉树
# 二叉树的深度
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
返回它的最大深度 3 。
# 法一:DFS
树的深度等于左子树的深度和右子树深度的最大值 + 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : int maxDepth (TreeNode* root) { if (root==nullptr ) return 0 ; int leftdeep=maxDepth (root->left); int rightdeep = maxDepth (root->right); return max (leftdeep,rightdeep)+1 ; } };
# 对称二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/
2 2
/ \ /
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/
2 2
\
3 3
题解:
双指针递归剪枝,结束条件为左右指针同时都为空指针返回 true,如果值不同或只有一个为空返回 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : bool isSymmetric (TreeNode* root) { if (root==NULL ) return true ; TreeNode *l=root; TreeNode *r=root; return recv (l,r); } bool recv (TreeNode * l,TreeNode *r) { if (l==NULL &&r==NULL ) return true ; if (l==NULL ||r==NULL ||l->val!=r->val) return false ; return recv (l->left,r->right)&&recv (l->right,r->left); } };
# 平衡二叉树
平衡二叉树的定义是:二叉树的每个节点的左右子树的高度差的绝对值不超过 11,则二叉树是平衡二叉树。根据定义,一棵二叉树是平衡二叉树,当且仅当其所有子树也都是平衡二叉树,因此可以使用递归的方式判断二叉树是不是平衡二叉树,递归的顺序可以是自顶向下或者自底向上。
# 法一: 自顶向下递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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,则 \texttt {height}§height§ 最多会被调用 dd 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 h 满足 O (h)=O (\log n) O (h)=O (logn),因为 d \leq hd≤h,所以总时间复杂度为 O (n \log n)。对于最坏的情况,二叉树形成链式结构,高度为 O (n),此时总时间复杂度为 O (n^2)
空间复杂度:O (n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
# 法二: 自底向上递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : int height (TreeNode* root) { if (root == NULL ) { return 0 ; } int leftHeight = height (root->left); int rightHeight = height (root->right); if (leftHeight == -1 || rightHeight == -1 || abs (leftHeight - rightHeight) > 1 ) { return -1 ; } else { return max (leftHeight, rightHeight) + 1 ; } } bool isBalanced (TreeNode* root) { return height (root) >= 0 ; } };
复杂度分析
时间复杂度:O (n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O (n)。
空间复杂度:O (n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
# 二叉树剪枝
后序遍历 dfs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution {public : TreeNode* pruneTree (TreeNode* root) { if (root == NULL ) return NULL ; TreeNode *leftnode = pruneTree (root->left); TreeNode *rightnode = pruneTree (root->right); if (root->val==0 && leftnode==NULL &&rightnode == NULL ) return nullptr ; root->left = leftnode; root->right= rightnode; return root; } };
# 寻找最近公共祖先
# 法一:递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : TreeNode* ans; bool dfs (TreeNode* root, TreeNode* p, TreeNode* q) { if (root == nullptr ) return false ; bool lson = dfs (root->left, p, q); bool rson = dfs (root->right, p, q); if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson))) { ans = root; } return lson || rson || (root->val == p->val || root->val == q->val); } TreeNode* lowestCommonAncestor (TreeNode* root, TreeNode* p, TreeNode* q) { dfs (root, p, q); return ans; } };
# 法二:存储父节点
从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。
从 p 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。
同样,我们再从 q 节点开始不断往它的祖先移动,如果有祖先已经被访问过,即意味着这是 p 和 q 的深度最深的公共祖先,即 LCA 节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : unordered_map<int , TreeNode*> fa; unordered_map<int , bool > vis; void dfs (TreeNode* root) { if (root->left != nullptr ) { fa[root->left->val] = root; dfs (root->left); } if (root->right != nullptr ) { fa[root->right->val] = root; dfs (root->right); } } TreeNode* lowestCommonAncestor (TreeNode* root, TreeNode* p, TreeNode* q) { fa[root->val] = nullptr ; dfs (root); while (p != nullptr ) { vis[p->val] = true ; p = fa[p->val]; } while (q != nullptr ) { if (vis[q->val]) return q; q = fa[q->val]; } return nullptr ; } };
# 二叉搜索树
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
二叉搜索树 转换成一个 “排序的循环双向链表” ,其中包含三个要素:
排序链表: 节点应从小到大排序,因此应使用 中序遍历 “从小到大” 访问树的节点。
双向链表: 在构建相邻节点的引用关系时,设前驱节点 pre 和当前节点 cur ,不仅应构建 pre.right = cur ,也应构建 cur.left = pre 。
循环链表: 设链表头节点 head 和尾节点 tail ,则应构建 head.left = tail 和 tail.right = head 。
dfs (cur): 递归法中序遍历;
终止条件: 当节点 cur 为空,代表越过叶节点,直接返回;
递归左子树,即 dfs (cur.left) ;
构建链表:
当 pre 为空时: 代表正在访问链表头节点,记为 head ;
当 pre 不为空时: 修改双向节点引用,即 pre.right = cur , cur.left = pre ;
保存 cur : 更新 pre = cur ,即节点 cur 是后继节点的 pre ;
递归右子树,即 dfs (cur.right) ;
treeToDoublyList(root):
特例处理: 若节点 root 为空,则直接返回;
初始化: 空节点 pre ;
转化为双向链表: 调用 dfs (root) ;
构建循环链表: 中序遍历完成后,head 指向头节点, pre 指向尾节点,因此修改 head 和 pre 的双向节点引用即可;
返回值: 返回链表的头节点 head 即可;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : Node* treeToDoublyList (Node* root) { if (root == nullptr ) return nullptr ; dfs (root); head->left = pre; pre->right = head; return head; } private : Node *pre, *head; void dfs (Node* cur) { if (cur == nullptr ) return ; dfs (cur->left); if (pre != nullptr ) pre->right = cur; else head = cur; cur->left = pre; pre = cur; dfs (cur->right); } };
复杂度分析:
时间复杂度 O (N): N 为二叉树的节点数,中序遍历需要访问所有节点。
空间复杂度 O (N) : 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O (N) 栈空间。