第一页总结

作者 : admin 本文共14795个字,预计阅读时间需要37分钟 发布时间: 2024-06-17 共1人阅读

第一页总结

  • 链表
    • 反转
      • 206. 反转链表
      • 25. K 个一组翻转链表
    • 双指针
      • 21. 合并两个有序链表
      • 141. 环形链表
  • dfs和回溯
    • 200. 岛屿数量
    • 46. 全排列
  • 二叉树
    • 102. 二叉树的层序遍历
    • 236. 二叉树的最近公共祖先
  • 字符串
    • 3. 无重复字符的最长子串
    • 5. 最长回文子串
  • 数组
    • 双指针
      • 1.两数之和
      • 15. 三数之和
      • 【二分】33. 搜索旋转排序数组
      • 88. 合并两个有序数组
    • 排序
      • 补充题4. 手撕快速排序
      • 215. 数组中的第K个最大元素
    • 动归
      • 121. 买卖股票的最佳时机
        • 贪心
        • 动归
      • 53. 最大子数组和
    • 有效的括号
      • 方法一:不用hashMap,只用栈
      • 方法二:用hashmap+栈

字节题目

链表

反转

206. 反转链表

206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。


递归思路:
每一个子递归都将当前节点下一个节点的下一个节点指向当前节点,当前节点的下一个节点置空。 递归终止条件:head为空或head.next为空。


复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。需要对链表的每个节点进行反转操作。
  • 空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 n 层。
class Solution {
    public ListNode reverseList(ListNode head) {
        if(head==null || head.next==null){
            return head;
        }
        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next=null;
        return newHead;
    }
}

迭代思路:用pre和cur两个指针遍历链表
复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度,需要遍历链表一次。
  • 空间复杂度:O(1)
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
}

25. K 个一组翻转链表

25. K 个一组翻转链表
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。


递归思路:k个一组翻转链表,对于每一组来说就是反转前n。所以可以采用递归的思想,首先找到下一个要翻转前n的节点,再翻转当前head的前n个节点,最后递归的反转后续链表并连接起来。


时间复杂度分析:
复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode p = head;
        // 找到下一个要反转前n的head
        for(int i=0;i<k;i++){
            if(p==null){
                return head;
            }
            p=p.next;
        }
        // 反转前N
        ListNode newHead = reverseN(head,k);
        head.next = reverseKGroup(p,k);
        return newHead;
    }

    /**
        反转前N
     */
    ListNode successor = null;
    private ListNode reverseN(ListNode head, int n){
        // 找到第一个不用反转的节点
        if(n==1){
            successor = head.next;
            return head;
        }
        ListNode newHead = reverseN(head.next, n-1);
        head.next.next = head;
        head.next = successor;
        return newHead;
    }
}

迭代:
时间复杂度:O(n),其中 n 为链表的长度。head 指针会在 O(⌊n/k⌋)个节点上停留,每次停留需要进行一次 O(k)的翻转操作。
空间复杂度:O(1).

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode hair = new ListNode(0);
        hair.next = head;
        ListNode pre = hair;

        while (head != null) {
            ListNode tail = pre;
            // 查看剩余部分长度是否大于等于 k
            for (int i = 0; i < k; ++i) {
                tail = tail.next;
                if (tail == null) {
                    return hair.next;
                }
            }
            ListNode nex = tail.next;
            ListNode[] reverse = myReverse(head, tail);
            head = reverse[0];
            tail = reverse[1];
            // 把子链表重新接回原链表
            pre.next = head;
            tail.next = nex;
            pre = tail;
            head = tail.next;
        }

        return hair.next;
    }

    public ListNode[] myReverse(ListNode head, ListNode tail) {
        ListNode prev = tail.next;
        ListNode p = head;
        while (prev != tail) {
            ListNode nex = p.next;
            p.next = prev;
            prev = p;
            p = nex;
        }
        return new ListNode[]{tail, head};
    }
}

双指针

相关题目:双指针秒杀七道链表题目

21. 合并两个有序链表

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


思路:比较两个链表的大小,将更小的接到p指针。

复杂度分析

  • 时间复杂度:O(n+m),其中 n和 m分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。

  • 空间复杂度:O(1)。我们只需要常数的空间存放若干变量。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        while(list1!=null && list2!=null){
            if(list1.val<list2.val){
                p.next = list1;
                list1 = list1.next;
            }else{
                p.next = list2;
                list2 = list2.next;
            }
            p = p.next;
        }
        if(list1!=null){
            p.next = list1;
        }
        if(list2!=null){
            p.next = list2;
        }
        return dummy.next;
    }
}

141. 环形链表

141. 环形链表
判断链表是否有环


思路:通过快慢指针,如果相遇则有环。
复杂度分析

  • 时间复杂度:O(N),其中N是链表中的节点数。
    当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
    当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 NNN 轮。

  • 空间复杂度:O(1)。我们只使用了两个指针的额外空间。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null){
            return false;
        }
        ListNode fast = head;
        ListNode slow = head;
        while(fast!=null && fast.next!=null){
            slow = slow.next;
            fast=fast.next.next;
            // 快慢指针相遇则有环
            if(fast==slow){
                return true;
            }
        }
        return false;
    }
}

dfs和回溯

200. 岛屿数量

labuladong题解
200. 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。


解题思路:遍历二维矩阵,遇到陆地则岛屿数+1,且通过dfs淹没其相邻的陆地。
复杂度分析

  • 时间复杂度:O(MN),其中 M 和 N分别为行数和列数。
  • 空间复杂度:O(MN),在最坏情况下,整个网格均为陆地,深度优先搜索的深度达到 MN。
class Solution {
    public int numIslands(char[][] grid) {
        int cnt = 0;
        // 遍历二维矩阵,找陆地、淹陆地
        for(int i=0; i<grid.length;i++){
            for(int j=0;j<grid[i].length;j++){
                if(grid[i][j]=='1'){
                    // 找到陆地,岛屿数++
                    cnt++;
                    // 淹没与其相邻的陆地
                    dfs(grid, i, j);
                }
            }
        }
        return cnt;
    }
    /**
        通过dfs淹没相邻陆地
     */
    private void dfs(char[][] grid, int i, int j){
        // 边界
        if(i<0 || i>=grid.length || j<0 || j>=grid[0].length){
            return ;
        }
        // 已淹没
        if(grid[i][j]=='0'){
            return ;
        }
        // 把i,j淹没,并递归淹没与其相邻的陆地
        grid[i][j] = '0';
        dfs(grid, i-1, j);
        dfs(grid, i+1, j);
        dfs(grid, i, j-1);
        dfs(grid, i, j+1);
    }
}

46. 全排列

46. 全排列

思路:遍历回溯树,每个合法答案都在叶子节点,遍历一遍树,把叶子节点的答案都收集起来就得到所有全排列的结果。
用路径来存当前已经做的选择,选择列表就是nums中未使用过的,结束条件就是遍历到了叶子节点也就是路径的大小和nums大小相同了。
回溯的过程就是在选择列表中做选择,然后递归,再撤销选择。

  • 时间复杂度 : O(n×n!),其中 nnn 为序列的长度。
    第一页总结插图

  • 空间复杂度:O(n),其中 n 为序列的长度。除答案数组以外,递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,这里可知递归调用深度为 O(n)。

class Solution {

    List<List<Integer>> res = new LinkedList<>();
    
    public List<List<Integer>> permute(int[] nums) {
        int n = nums.length;
        // 通过是否使用过 决定可选择列表
        boolean[] used = new boolean[n];
        // 记录路径
        List<Integer> track = new LinkedList<>();
        backtrack(nums,track,used);
        return res;
    }

    public void backtrack(int[] nums, List<Integer> track, boolean[] used){
        // 结束条件:已经走到叶子节点
        if(track.size()==nums.length){
            res.add(new LinkedList(track));
            return ;
        }
        // 选择列表:nums中排除已经用过的
        for(int i = 0;i<nums.length;i++){
            // 已使用过 不能选择
            if(used[i]){
                continue;
            }
            // 做选择
            used[i] = true;
            track.add(nums[i]);
            // 继续遍历
            backtrack(nums, track, used);
            // 撤销选择
            used[i] = false;
            track.removeLast();
        }
    }
}

二叉树

102. 二叉树的层序遍历

102. 二叉树的层序遍历
思路:通过队列来实现二叉树的层序遍历,通过while循环判断队列是否为空控制二叉树从上到下的遍历,根据当前队列的size用for循环控制每一层从左到右的遍历。

时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new LinkedList<>();
        if(root==null){
            return result;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        // while循环控制从上向下遍历
        while(!queue.isEmpty()){
            int size = queue.size();
            // for循环控制该层从左到右遍历
            List<Integer> curRes = new ArrayList<>();
            for(int i=0;i<size;i++){
                TreeNode node = queue.poll();
                curRes.add(node.val);
                if(node.left!=null){
                    queue.offer(node.left);
                }
                if(node.right!=null){
                    queue.offer(node.right);
                }   
            }
            result.add(curRes);

        }
        return result;
    }
}

236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。p和q一定存在于二叉树中。
最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”


思路:找p和q的公共祖先,就是找以root为根的二叉树中寻找p或q的节点。如果root为p或q直接返回root,接着递归的遍历左右子树,如果在左右子树分别找到了p和q则返回root,如果p和q都没在以root为根的节点找到则返回null,如果p和q存在一个则返回那个节点。
复杂度分析:

  • 时间复杂度:O(N),其中 N是二叉树的节点数。二叉树的所有节点有且只会被访问一次,因此时间复杂度为 O(N)。
  • 空间复杂度:O(N) ,其中 N是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N)。
class Solution {
    //定义:在以 root 为根的二叉树中寻找值为 p 或 q 的节点
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null){
            return null;
        }
        // base case: 找到p或q
        if(root.val==p.val || root.val==q.val){
            return root;
        }
        // 从root的左右子树找
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        // 情况一:分别在左右子树找到了p和q
        if(left!=null && right!=null){
            return root;
        }
        return left==null?right:left;
    }
}

字符串

3. 无重复字符的最长子串

3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长
子串的长度。


思路:采用滑动窗口的思想,通过窗口存储窗口内字符出现的个数,扩张窗口就讲window对应的key+1,当遇到字符个数大于1时收缩窗口,直到字符个数等于1时更新答案。
时间复杂度:O(N)
空间复杂度:O(1),字符的 ASCII 码范围为 0~127,哈希表最多使用 O(128)=O(1)大小的额外空间。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        HashMap<Character,Integer> window = new HashMap<>();
        int maxLen = 0;
        int left=0,right=0;
        while(right<n){
            // 扩张窗口
            Character c = s.charAt(right);
            right++;
            // 更新窗口数据
            window.put(c, window.getOrDefault(c,0)+1);
            // 有重复数据时收缩窗口
            while(window.get(c)>1){
                Character d = s.charAt(left);
                left++;
                // 更新窗口数据
                if(window.containsKey(d)){
                    window.put(d, window.get(d)-1);
                }
            }
            // 更新结果
            maxLen = Math.max(maxLen, right-left);
        }
        return maxLen;
    }
}

5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。


方法一:双指针 中心扩展法
遍历字符串,分别找s[i] 和s[i] 和 s[i+1]为中心的回文串,找到最长的。
寻找回文串的方法:从中间向两边扩散。
时间复杂度:O(n^2)。
空间复杂度:O(1)。

class Solution {
    public String longestPalindrome(String s) {
        String res = "";
        for(int i=0;i<s.length();i++){
            // 以i为中心的最长回文串
            String s1 = findPalindrome(s,i,i);
            // 以i,i+1为中心的最长回文串
            String s2 = findPalindrome(s,i,i+1);
            // 找到最长
            res = s1.length()>res.length()?s1:res;
            res = s2.length()>res.length()?s2:res;
        }
        return res;
    }

    /**
        从中间向两边扩散寻找回文串
     */
    public String findPalindrome(String s, int l, int r){
        while(l>=0 && r<s.length() && s.charAt(l)==s.charAt(r)){
            l--;
            r++;
        }
        // 找到以l,r为中心的最长回文串
        return s.substring(l+1,r);
    }
}

数组

双指针

拓展:一个方法团灭nSum问题

1.两数之和

1.两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。


思路:用map维护数组值和下标的映射关系,遍历数组的过程中如果map包含target-nums[i]则返回对应下标,没有则将当前遍历的值put到map中。
复杂度:

  • 时间复杂度 O(n)
  • 空间复杂度 O(n)
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 维护值和下标的映射
        HashMap<Integer,Integer> map = new HashMap<>();
        for(int i=0;i<nums.length;i++){
            int need = target-nums[i];
            if(map.containsKey(need)){
                return new int[]{map.get(need),i};
            }
            map.put(nums[i],i);
        }
        return new int[]{-1,-1};
    }
}

15. 三数之和

解题思路:排序+双指针,并且在遍历的过程中去重
时间复杂度:O(N^2)
空间复杂度:O(log⁡N)。我们忽略存储答案的空间,额外的排序的空间复杂度为 O(log⁡N)。

class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for(int i=0;i<n;i++){
int left = i+1;
int right = n-1;
while(left<right){
int leftValue = nums[left];
int rightValue = nums[right];
int sum = nums[i]+leftValue+rightValue;
if(sum==0){
// 找到可行解
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[left]);
list.add(nums[right]);
res.add(list);
while(left<right && nums[right]==rightValue){
right--;
}
while(left<right && nums[left]==leftValue){
left++;
}
}else if(sum>0){
while(left<right && nums[right]==rightValue){
right--;
}
}else if(sum<0){
while(left<right && nums[left]==leftValue){
left++;
}
}
}
while(i+1<n && nums[i]==nums[i+1]){
i++;
}
}
return res;
}
}

【二分】33. 搜索旋转排序数组

33. 搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值互不相同 。
给你旋转后的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。


思路:对有序的数组可以直接用二分查找,虽然这个有序数组被旋转了一次,但是左右两半一定会有一部分是有序的,所以只有找到有序的那一半还是可以用二分。
时间复杂度: O(log⁡n)。
空间复杂度: O(1) 。

class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
int left=0,right=n-1;
while(left<=right){
int mid = (right-left)/2+left;
// 找到答案
if(nums[mid]==target){
return mid;
}
// 左半部分有序 [left,mid)
if(nums[mid]>=nums[left]){
if(target>=nums[left] && target<nums[mid]){
right = mid-1;
}else{
left = mid+1;
}
}else{
// 右半部分有序(mid,right]
if(target>nums[mid] && target<=nums[right]){
left = mid+1;
}else{
right = mid-1;
}
}
}
return -1;
}
}

88. 合并两个有序数组

88. 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。


思路:从后往前遍历数组,这样可以避免nums1的数据被覆盖。遍历过程中如果nums2已经遍历完说明都已经插入到相应的位置,未遍历完如果nums1遍历完则直接插nums2,如果nums1未遍历完则与nums1比大小谁大插谁。
时间复杂度:O(m+n)。
空间复杂度:O(1)。

class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int l1 = m-1;
int l2 = n-1;
// 从后往前遍历
for(int i=m+n-1; i>=0;i--){
// nums2已全部插入
if(l2<0){
return ;
}
// 情况1 :nums1已经插完,则无脑插nums2;nums1没插完,如果nums2大就插
if(l1>=0 && nums1[l1]<nums2[l2] || l1<0){
nums1[i] = nums2[l2];
l2--;
}else{
nums1[i] = nums1[l1];
l1--;
}
}
}
}

排序

补充题4. 手撕快速排序

912. 排序数组
给你一个整数数组 nums,请你将该数组升序排列。

采用快排来做:快排的思想就是找到一个分区点,使其左边的数都小于他,右边的数都大于他,这样这个分区点就排好序了,然后再递归的排序分区点左半部分和右半部分。
分区的过程就是先随机找一个分区点,然后通过游标i将数组分为已排序区[lo,i-1]和未排序区两部分[i,hi-1],遍历未排序区,如果小于分区点的值则和i交换置于已排序区的尾部。
最后再把分区点和i交换,让分区点到正确的位置上。
**时间复杂度:O(nlogn)
空间复杂度:O(h)。**其中 h 为快速排序递归调用的层数。我们需要额外的 O(h) 的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n)的空间,最优情况下每次都平衡,此时整个递归树高度为logn,空间复杂度为 O(log⁡n)。

class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
private void quickSort(int[] nums, int lo, int hi){
// base case
if (lo>=hi){
return ;
}
// 分区
int p = randomizedPartition(nums, lo, hi);
// 递归的排序左半部分和右半部分
quickSort(nums,lo,p-1);
quickSort(nums,p+1,hi);
}
private int randomizedPartition(int[] nums, int lo, int hi){
// 随机选择分区点
int i = new Random().nextInt(hi-lo+1)+lo;
swap(nums, i, hi);
return partition(nums,lo,hi);
}
/**
将数组分区,找到一个分区点使得左半部分都小于分区点,右半部分都大于分区点
*/
private int partition(int[] nums, int lo, int hi){
// 数组的最后一个元素作为分区点
int pivot = nums[hi];
int i=lo;
// [lo,i] 已处理区域  [i,hi]未处理区域
for(int j=lo;j<=hi-1;j++){
if(nums[j]<pivot){
swap(nums, i, j);
// 处理一个了,已处理区域向前移动
i++;
}
}
// 把分区点换到对应位置
swap(nums,i,hi);
return i;
}
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}

215. 数组中的第K个最大元素

215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。


方法一:用小顶堆
遍历一遍数组,把元素放到小顶堆,当堆内元素大于k时删除堆顶元素,这样最后剩下的就是k个最大元素,堆顶就是第k个最大元素

二叉堆插入和删除的时间复杂度和堆中的元素个数有关,在这里我们堆的大小不会超过 k,所以插入和删除元素的复杂度是 O(logK),再套一层 for 循环,总的时间复杂度就是 O(NlogK)。
空间复杂度:O(log⁡n)

class Solution {
public int findKthLargest(int[] nums, int k) {
// 小顶堆
PriorityQueue<Integer> pq = new PriorityQueue<>();
int n = nums.length;
for(int i=0;i<n;i++){
pq.offer(nums[i]);
// 堆的元素大于k,则删除堆顶元素
if(pq.size()>k){
pq.poll();
}
}
// pq中剩下的就是nums中k个最大元素,堆顶元素就是k个最大元素
return pq.peek();
}
}

动归

121. 买卖股票的最佳时机

【只交易一次 所以很简单】
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

贪心

最好的结果就是在最低点买入,在收益最高的时候卖出。
做法就是在遍历数据的时候,更新最低价,更新前i天的最高利润。
时间复杂度:O(n)
空间复杂度:O(1)

class Solution {
public int maxProfit(int[] prices) {
int minCost = Integer.MAX_VALUE;
int maxProfit = 0;
for(int i=0;i<prices.length;i++){
// 找最低价格
minCost = Math.min(minCost, prices[i]);
// 找最高收益
maxProfit = Math.max(maxProfit, prices[i]-minCost);
}
return maxProfit;
}
}
动归

dp[i][j] 表示天数 [0, i] 区间里,下标 i 这一天状态为 j 的时候能够获得的最大利润。
base case:dp[i][0] = 0 dp[i][1] = -prices[i],没买就是0,买了就是当天股价的相反数
转移方程:
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
复杂度分析:
时间复杂度:O(N),遍历股价数组可以得到最优解;
空间复杂度:O(N),状态数组的长度为 N。

还可以把dp[i][j]压缩成一维数组,使状态数组的长度为2,空间复杂度为O(1)。

class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
// dp[i][0或1] 第i天买入的 获得最大利润
int[][] dp = new int[n][2]; 
// 转移方程
// dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
// dp[i][1] = max(dp[i-1][1], -prices[i])
// base case dp[i][0] = 0  dp[i][1] = -prices[i];
for(int i=0;i<n;i++){
// base case
if(i==0){
// 不持股为0
dp[i][0] = 0;
// 持股为当天股价的相反数,因为只交易一次
dp[i][1] = -prices[i];
continue;
}
// 转移方程
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n-1][0];
}
}

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。


定义一个dp[i] 表示以i结尾的连续子数组的和为dp[i],每个dp[i] 有两个选择,接着前面的 和自己一个人。

复杂度分析:
时间复杂度:O(N),;
空间复杂度:O(N),状态数组的长度为 N。

class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
// dp[i] 以i结尾的最大和
int[] dp = new int[n];
dp[0] = nums[0];
for(int i=1;i<n;i++){
// 自己作为起始, 续上前面的
dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
}
return Arrays.stream(dp).max().getAsInt();
}
}

可以状态压缩下

复杂度分析:
时间复杂度:O(N)
空间复杂度:O(1)

class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int maxAns=nums[0];
int preSum = 0;
for(int i=0;i<n;i++){
preSum = Math.max(nums[i], preSum+nums[i]);
maxAns = Math.max(maxAns, preSum);
}
return maxAns;
}
}

有效的括号

方法一:不用hashMap,只用栈

思路:用栈存左括号,如果遇到右括号则判断栈顶是否为其对应的左括号。
时间复杂度:O(n)
空间复杂度:O(n)

class Solution {
public boolean isValid(String s) {
if(s.length()%2!=0){
return false;
}
Stack<Character> stack = new Stack<>();
for(int i=0;i<s.length();i++){
Character c = s.charAt(i);
if(c=='(' || c=='[' || c=='{'){
stack.push(c);
}else{
if(stack.isEmpty()){
return false;
}
Character top = stack.pop();
if(!(top=='(' && c==')' || top=='[' && c==']' || top=='{' && c=='}' )){
return false;
}
}
}
return stack.isEmpty();
}
}

方法二:用hashmap+栈

思路:首先用map存括号对,key为右括号用于匹配;用栈存左括号;遍历字符串遇左括号入栈,遇右括号则判断栈不为空且栈顶元素和当前key对应value匹配。
时间复杂度:O(n)
空间复杂度:O(n+6)。栈中的字符数量为 O(n),而哈希表使用的空间为 O(∣Σ∣),本题中字符串只包含 6种括号,∣Σ∣=6。相加即可得到总空间复杂度。

class Solution {
public boolean isValid(String s) {
int n = s.length();
if(n%2!=0){
return false;
}
// 存左括号
Stack<Character> stack = new Stack<>();
// 存括号对
HashMap<Character,Character> pairs = new HashMap<>(){{
put(')','(');
put(']','[');
put('}','{');
}};
for(Character c : s.toCharArray()){
if(pairs.containsKey(c)){
// 说明是右括号
if(stack.isEmpty() || stack.peek()!=pairs.get(c)){
return false;
}
stack.pop();
}else{
// 说明是左括号,直接入栈
stack.push(c);
}
}
return stack.isEmpty();
}
}
本站无任何商业行为
个人在线分享 » 第一页总结
E-->