199. 二叉树的右视图
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例:
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:
1 <---
/ \
2 3 <---
\ \
5 4 <---
方法一:深度优先搜索
我们对树进行深度优先搜索,在搜索过程中,我们总是先访问右子树。那么对于每一层来说,我们在这层见到的第一个结点一定是最右边的结点。 复杂度分析
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Map<Integer, Integer> rightmostValueAtDepth = new HashMap<Integer, Integer>();
int max_depth = -1;
Stack<TreeNode> nodeStack = new Stack<TreeNode>();
Stack<Integer> depthStack = new Stack<Integer>();
nodeStack.push(root);
depthStack.push(0);
while (!nodeStack.isEmpty()) {
TreeNode node = nodeStack.pop();
int depth = depthStack.pop();
if (node != null) {
// 维护二叉树的最大深度
max_depth = Math.max(max_depth, depth);
// 如果不存在对应深度的节点我们才插入
if (!rightmostValueAtDepth.containsKey(depth)) {
rightmostValueAtDepth.put(depth, node.val);
}
nodeStack.push(node.left);
nodeStack.push(node.right);
depthStack.push(depth + 1);
depthStack.push(depth + 1);
}
}
List<Integer> rightView = new ArrayList<Integer>();
for (int depth = 0; depth <= max_depth; depth++) {
rightView.add(rightmostValueAtDepth.get(depth));
}
return rightView;
}
}
时间复杂度 : O(n)。深度优先搜索最多访问每个结点一次,因此是线性复杂度。
空间复杂度 : O(n)。最坏情况下,栈内会包含接近树高度的结点数量,占用 O(n) 的空间。
方法二:广度优先搜索
思路
我们可以对二叉树进行层次遍历,那么对于每层来说,最右边的结点一定是最后被遍历到的。二叉树的层次遍历可以用广度优先搜索实现。
算法
执行广度优先搜索,左结点排在右结点之前,这样,我们对每一层都从左到右访问。因此,只保留每个深度最后访问的结点,我们就可以在遍历完整棵树后得到每个深度最右的结点。除了将栈改成队列,并去除了rightmost_value_at_depth之前的检查外,算法没有别的改动。
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Map<Integer, Integer> rightmostValueAtDepth = new HashMap<Integer, Integer>();
int max_depth = -1;
Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();
Queue<Integer> depthQueue = new LinkedList<Integer>();
nodeQueue.add(root);
depthQueue.add(0);
while (!nodeQueue.isEmpty()) {
TreeNode node = nodeQueue.remove();
int depth = depthQueue.remove();
if (node != null) {
// 维护二叉树的最大深度
max_depth = Math.max(max_depth, depth);
// 由于每一层最后一个访问到的节点才是我们要的答案,因此不断更新对应深度的信息即可
rightmostValueAtDepth.put(depth, node.val);
nodeQueue.add(node.left);
nodeQueue.add(node.right);
depthQueue.add(depth + 1);
depthQueue.add(depth + 1);
}
}
List<Integer> rightView = new ArrayList<Integer>();
for (int depth = 0; depth <= max_depth; depth++) {
rightView.add(rightmostValueAtDepth.get(depth));
}
return rightView;
}
}
时间复杂度 : O(n)。 每个节点最多进队列一次,出队列一次,因此广度优先搜索的复杂度为线性。
空间复杂度 : O(n)。每个节点最多进队列一次,所以队列长度最大不不超过 nn,所以这里的空间代价为 O(n)。
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
方法一:动态规划
思路与算法
定义 为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 必须被选取。
我们从小到大计算 数组的值,在计算 之前,我们已经计算出 的值,则状态转移方程为:
即考虑往 中最长的上升子序列后面再加一个 。由于 代表 中以 结尾的最长上升子序列,所以如果能从 这个状态转移过来,那么 必然要大于 ,才能将 放在 后面以形成更长的上升子序列。
最后,整个数组的最长上升子序列即所有 中的最大值。
class Solution{
public int lengthOfLIS(int[] nums){
if(nums.length == 0){
return 0;
}
int[] dp = new int[nums.length];
dp[0] = 1;
int maxans = 1;
for(int i = 1;i < nums.length; i++){
dp[i] = 1;
for(int j = 0;j <i;j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
maxans = Math.max(maxans,dp[i]);
}
return maxans;
}
}
时间复杂度:,其中 n 为数组 的长度。动态规划的状态数为 n,计算状态 时,需要 O(n) 的时间遍历 的所有状态,所以总时间复杂度为 。
空间复杂度:O(n),需要额外使用长度为 n 的 dp 数组。
方法二:贪心 + 二分查找
思路与算法
考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
基于上面的贪心思路,我们维护一个数组 d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值,用 len 记录目前最长上升子序列的长度,起始时 len 为 1,d[1]=nums[0]。
同时我们可以注意到 d[i] 是关于 i 单调递增的。因为如果 d[j]≥d[i] 且 j < i,我们考虑从长度为 i 的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 d[i],也就小于 d[j]。那么我们就找到了一个长度为 j 的最长上升子序列,并且末尾元素比 d[j] 小,从而产生了矛盾。因此数组 d 的单调性得证。
我们依次遍历数组 nums 中的每个元素,并更新数组 d 和 len 的值。如果 nums[i]>d[len] 则更新 len = len + 1,否则在 中找满足 的下标 i,并更新。
根据 d 数组的单调性,我们可以使用二分查找寻找下标 i,优化时间复杂度。
最后整个算法流程为:
设当前已求出的最长上升子序列的长度为 len(初始时为 11),从前往后遍历数组 nums,在遍历到 nums[i] 时:
如果 nums[i]>d[len] ,则直接加入到 dd 数组末尾,并更新 len=len+1;
否则,在 d 数组中二分查找,找到第一个比 nums[i] 小的数 d[k] ,并更新 d[k+1]=nums[i]。
以输入序列 [0,8,4,12,2] 为例:
第一步插入 0,d = [0];
第二步插入 8,d = [0, 8];
第三步插入 4,d = [0, 4];
第四步插入 12,d = [0, 4, 12];
第五步插入 2,d = [0, 2, 12]。
最终得到最大递增子序列长度为 3。
class Solution{
public int lengthOfLIS(int[] nums){
int len = 1, n = nums.length;
if(n == 0){
return 0;
}
int[] d = new int[n+1];
for(int i = 1; i < n; ++i){
if(nums[i] > d[len]){
d[++len] = nums[i];
}else{
int l = 1, r = len, pos = 0;
while(l<=r){
int mid = (l+r)>>1;
if(d[mid]<nums[i]){
pos = mid;
l = mid + 1;
}else{
r = mid - 1;
}
}
d[pos+1] = nums[i];
}
}
return len;
}
}
时间复杂度:O(nlogn)。数组 nums 的长度为 n,我们依次用数组中的元素去更新 d 数组,而更新 d 数组时需要进行 O(logn) 的二分搜索,所以总时间复杂度为 O(nlogn)。
空间复杂度:O(n),需要额外使用长度为 n 的 d 数组。
两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
var addTwoNumbers = function(l1,l2){
let head = null,tail = null;
let carry = 0;
while(l1||l2){
const n1 = l1?l1.val:0;
const n2 = l2?l2.val:0;
const sum = n1+n2+carry;
if(!head){
head = tail = new ListNode(sum%10);
}else{
tail.next = new ListNode(sum%10);
tail = tail.next;
}
carry = Math.floor(sum/10);
if(l1){
l1 = l1.next;
}
if(l2){
l2 = l2.next;
}
}
if(carry > 0){
tail.next = new ListNode(carry);
}
return head;
};
复杂度分析
时间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间。
空间复杂度:O(1)。注意返回值不计入空间复杂度。
151. 翻转字符串里的单词
给你一个字符串 s ,逐个翻转字符串中的所有 单词 。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
请你返回一个翻转 s 中单词顺序并用单个空格相连的字符串。
说明:
输入字符串 s 可以在前面、后面或者单词间包含多余的空格。 翻转后单词间应当仅用一个空格分隔。 翻转后的字符串中不应包含额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:输入字符串可以在前面或者后面包含多余的空格,但是翻转后的字符不能包括。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,将翻转后单词间的空格减少到只含一个。
示例 4:
输入:s = " Bob Loves Alice "
输出:"Alice Loves Bob"
示例 5:
输入:s = "Alice does not even like bob"
输出:"bob like even not does Alice"
方法一:使用语言特性
思路和算法
很多语言对字符串提供了 split(拆分),reverse(翻转)和 join(连接)等方法,因此我们可以简单的调用内置的 API 完成操作:
使用 split 将字符串按空格分割成字符串数组; 使用 reverse 将字符串数组进行反转; 使用 join 方法将字符串数组拼成一个字符串。
var reverseWords = function(s) {
return s.trim().split(/\s+/).reverse().join(' ');
};
复杂度分析
时间复杂度:O(N),其中 N 为输入字符串的长度。
空间复杂度:O(N),用来存储字符串分割之后的结果。
方法二:自行编写对应的函数
思路和算法
我们也可以不使用语言中的 API,而是自己编写对应的函数。在不同语言中,这些函数实现是不一样的,主要的差别是有些语言的字符串不可变(如 Java 和 Python),有些语言的字符串可变(如 C++)。
对于字符串不可变的语言,首先得把字符串转化成其他可变的数据结构,同时还需要在转化的过程中去除空格。
public StringBuilder trimSpaces(String s){
int left = 0,right = s.length()-1;
//去掉字符串开头空白字符
while(left <= right &&s.charAt(left)== ' '){
++left;
}
//去掉字符串末尾的空白字符
while(left <= right && s.charAt(right)==' '){
--right;
}
//去多余字符
StringBuilder sb = new StringBuilder();
while(left <= right){
char c = s.charAt(left);
if(c !=' '){
sb.append(c);
}else if (sb.charAt(sb.length()-1)!=' '){
sb.append(c);
}
++left;
}
return sb;
}
public void reverse(StringBuilder sb,int left,int right){
while(left < right){
char tmp = sb.charAt(left);
sb.setCharAt(left++;sb.charAt(end)!=' ');
sb.setCharAt(right--,tmp);
}
}
public void reverseEachWord(StringBuilder sb){
int n = sb.length();
int start = 0,end = 0;
while(start<n){
//至单词结尾
while(end <n&&sb.charAt(end)!=' '){
++end;
}
//翻转单词
reverse(sb,start,end -1);
start = end+1;
++end;
}
}
}
复杂度分析
时间复杂度:O(N),其中 N 为输入字符串的长度。
空间复杂度:Java 和 Python 的方法需要 O(N) 的空间来存储字符串,而 C++ 方法只需要 O(1) 的额外空间来存放若干变量。
方法三:双端队列
思路和算法
由于双端队列支持从队列头部插入的方法,因此我们可以沿着字符串一个一个单词处理,然后将单词压入队列的头部,再将队列转成字符串即可。
class Solution{
public String reverseWords(String s){
int left = 0,right = s.length()-1;
while(left<=right&&s.charAt(left)==' '){
++left;
}
while(left <= right&&s.charAt(right==' ')){
--right;
}
Deque<String> d = new ArrayDeque<String>();
StringBuilder word = new StringBuilder();
while(left <= right){
char c = s.charAt(left);
if((word.length()!=0)&&(c==' ')){
d.offerFirst(word.toString());
word.setLength(0);
}else if (c!=' '){
word.append(c);
}
++left;
}
d.offerFirst(word.toString());
return String.join(" ",d);
}
}
复杂度分析
时间复杂度:O(N),其中 N 为输入字符串的长度。
空间复杂度:O(N),双端队列存储单词需要 O(N) 的空间
8. 字符串转换整数 (atoi)
根据问题的描述我们来判断并且描述对应的解题方法。对于这道题目,很明显是字符串的转化问题。需要明确转化规则,尽量根据转化规则编写对应的子函数。
这里我们要进行模式识别,一旦涉及整数的运算,我们需要注意溢出。本题可能产生溢出的步骤在于推入,乘 10 操作和累加操作都可能造成溢出。对于溢出的处理方式通常可以转换为 INT_MAX 的逆操作。比如判断某数乘 10 是否会溢出,那么就把该数和 INT_MAX 除 10 进行比较。
方法一:自动机
思路
字符串处理的题目往往涉及复杂的流程以及条件情况,如果直接上手写程序,一不小心就会写出极其臃肿的代码。
因此,为了有条理地分析每个输入字符的处理方法,我们可以使用自动机这个概念:
我们的程序在每个时刻有一个状态 s,每次从序列中输入一个字符 c,并根据字符 c 转移到下一个状态 s'。这样,我们只需要建立一个覆盖所有情况的从 s 与 c 映射到 s' 的表格即可解决题目中的问题。
class Solution{
public int myAuto(String str){
Automaton automaton = new Automaton();
int length = str.length();
for(int i = 0;i<length;++i){
automaton.get(str.charAt(i));
}
return (int)(automaton.sign*automaton.ans);
}
}
class Automaton{
public int sign = 1;
public long ans = 0;
private String state = "start":
private Map<String,String[]>table = new HashMap<String,String[]>(){{
put("start", new String[]{"start", "signed", "in_number", "end"});
put("signed", new String[]{"end", "end", "in_number", "end"});
put("in_number", new String[]{"end", "end", "in_number", "end"});
put("end", new String[]{"end", "end", "end", "end"});
}};
public void get(char c){
state = table.get(state)[get_col(c)];
if("in_number".equals(State)){
ans = ans*10+c-'0';
ans = sign == 1?Math.min(ans, (long)Integer.MAX_VALUE):Math.min(ans,-(long)Integer.MIN_VALUE);
}else if ("signed".equals(state)){
sign = c =='+'?1:-1;
}
}
private int get_col(char c){
if(c == ' '){
return 0;
}
if(c=='+'||c=='-'){
return 1;
}
if(Character.isDigit(c)){
return 2;
}
return 3;
}
}
复杂度分析
时间复杂度:O(n),其中 nn 为字符串的长度。我们只需要依次处理所有的字符,处理每个字符需要的时间为 O(1)。
空间复杂度:O(1),自动机的状态只需要常数空间存储。