第一页总结

字节题目

146. LRU 缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。


方法一:使用内置函数LinkedHashMap

  • get时,如果cache不包含就返回-1,包含置为最近使用,再返回value
  • put时,如果key本来存在则更新为新value,再置为最近使用;如果满了,则找到链表头,删掉,再put。没满直接put
  • 置为最近使用就是,先删掉最后一个,再put进去。
class LRUCache {

    int cap;
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
    public LRUCache(int capacity) {
        this.cap = capacity;
    }
    
    public int get(int key) {
        if(cache.containsKey(key)){
            makeRecently(key);
            return cache.get(key);
        }else{
            // 不包含 返回-1
            return -1;
        }
        // 如果存在,把key放到链表的尾端
    }
    
    public void put(int key, int value) {
        // 如果key存在,则更新value,并作为最近使用key
        if(cache.containsKey(key)){
            cache.put(key,value);
            makeRecently(key);
            return ;
        }

        // 如果满了删除表头
        if(cache.size()>=cap){
            // 找到最久未使用的
            int oldestKey = cache.keySet().iterator().next();
            cache.remove(oldestKey);
        }
        // 再put;
        cache.put(key,value);
    }


    public void makeRecently(int key){
        int value = cache.get(key);
        cache.remove(key);
        cache.put(key,value);
    }
}

方法二:用双向链表+map做


import java.util.HashMap;

class Node {
    public int key,val;

    public Node prev, next;

    public Node(int k, int v){
        this.key = k;
        this.val = v;
    }
}


class DoubleList{
    private Node head,tail;
    // 链表个数
    private int size;
    // 初始化链表
    public DoubleList(){
        head = new Node(0,0);
        tail = new Node(0,0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }
    // 向尾部插入元素
    public void addLast(Node x){
        x.prev = tail.prev;
        tail.prev.next = x;
        x.next = tail;
        tail.prev = x;
        size++;
    }
    // 删除链表的x节点
    public void remove(Node x){
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }
    // 删除链表中的第一个节点,并返回删除的元素
    public Node removeFirst(){
        if (head.next==tail){
            return null;
        }
        Node first = head.next;
        remove(first);
        return first;
    }
    public int size(){
        return size;
    }
}
class LRUCache {
    // hash表
    private HashMap<Integer,Node> map;
    // 双向链表
    private DoubleList cache;
    // 最大容量
    private int cap;

    public LRUCache(int capacity) {

        cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }
    
    public int get(int key) {
        // 不存在 返回-1
        if (!map.containsKey(key)){
            return -1;
        }
        Node node = map.get(key);
        // 变成最近使用
        makeRecently(key);
        return node.val;
    }
    
    public void put(int key, int value) {

        // 1、key 存在
        if (map.containsKey(key)){
            // 删除旧数据
            deleteKey(key);
            // 将新数据置为最近使用
            addRecently(key,value);
            return ;
        }
        // 2、key不存在
        // 当前容量是否已满,满了就删除最久未使用的节点
        if (cache.size()==cap){
            removeLeastRecently();
        }
        // 将新数据置为最近使用
        addRecently(key,value);
        
    }

    // 将某个key提升为最近最久使用
    public void makeRecently(int key){
        Node x = map.get(key);
        // 先从链表中删除这个节点
        cache.remove(x);
        // 插入到队尾
        cache.addLast(x);
    }

    // 添加最近使用
    public void addRecently(int key, int val){
        // 插入map
        Node x = new Node(key,val);
        map.put(key,x);
        // 添加到队尾
        cache.addLast(x);
    }

    // 删除key
    public void deleteKey(int key){
        Node x = map.get(key);
        cache.remove(x);
        map.remove(key);
    }

    // 删除最久未使用
    public void removeLeastRecently(){
        Node node = cache.removeFirst();
        map.remove(node.key);
    }
}

链表

反转

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();
    }
}

方法二:用快速选择排序
由于是找倒数第k个排好序的元素,快排的原理就是找一个分区点,这个分区点的左边比分区点小,右边比分区点大。基于此,我们可以在快排的基础上改进:

  • 分区点索引正好是 N−k ,则意味着它就是第 k大的数字 。此时就可以直接返回它,无需继续递归下去了。
  • 如果分区点索引小于n-k,则只需递归右半分区;如果分区点索引大于n-k,则只需递归左半分区
    在这里插入图片描述
class Solution {
    public int findKthLargest(int[] nums, int k) {
        int target = nums.length-k;
        quickSort(nums,0,nums.length-1,target);
        return nums[target];
    }


    public void quickSort(int[] nums, int lo, int hi, int target){
        if(lo>=hi){
            return ;
        }
        int p = getRandomPartition(nums,lo,hi);
        // 分区点是target说明已经找到了
        if(target==p){
            return;
        }
        // 只需去左边找
        if(p>target){
            quickSort(nums,lo,p-1,target);
            return;
        }
        // 只需去右边找
        if(p<target){
            quickSort(nums,p+1,hi,target);
            return ;
        }
        
    }

    public int getRandomPartition(int[] nums, int lo, int hi){
        int p = new Random().nextInt(hi-lo+1)+lo;
        swap(nums,p,hi);
        return getPartition(nums,lo,hi);
    }

    public int getPartition(int[] nums, int lo, int hi){
        int i=lo;
        int pivot = nums[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;
    }
}

动归

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 preSum = 0;
        int maxSum=Integer.MIN_VALUE;
        for(int i=0;i<nums.length;i++){
            // 选择接着前面 或自己一个
            preSum = Math.max(preSum+nums[i], nums[i]);
            maxSum = Math.max(maxSum, preSum);
        }
        return maxSum;
    }
}

有效的括号

方法一:不用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();
    }
}
  • 15
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以使用pandas库中的to_excel()函数将DataFrame导出到Excel文件的某一个工作表。 示例代码如下: ```python import pandas as pd # 创建DataFrame df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6], 'C': [7, 8, 9]}) # 导出到Excel文件的第一个工作表 df.to_excel('output.xlsx', sheet_name='Sheet1') ``` 在上述代码中,我们将df导出到了一个名为'output.xlsx' 的Excel文件中的第一个工作表 'Sheet1' ### 回答2: 要将DataFrame导出到Excel中的一页,可以使用Python中的pandas库来操作。 首先,我们需要导入pandas库和DataFrame的数据。假设我们有一个叫做df的DataFrame变量,其中包含了我们要导出的数据。 接下来,我们可以使用pandas的to_excel()函数来将DataFrame导出到Excel文件。这个函数接受一个文件路径作为参数,指定要保存到的Excel文件的路径和名称,后缀名通常为.xlsx。例如,我们可以将DataFrame导出到名为"output.xlsx"的文件中。 代码示例如下: import pandas as pd # 假设我们的DataFrame变量名为df # 将DataFrame导出到Excel中的一页 df.to_excel("output.xlsx", sheet_name="Sheet1", index=False) 在这段代码中,我们使用to_excel()函数将DataFrame导出到Excel文件中。其中的参数sheet_name="Sheet1"表示我们要将DataFrame导出到Excel的第一页,index=False表示不导出行索引。 最后,当我们运行完这段代码后,就会在我们指定的路径下生成一个名为"output.xlsx"的Excel文件,其中包含了DataFrame的数据。 总结起来,要将DataFrame导出到Excel其中一页,我们需要导入pandas库和DataFrame数据,并使用pandas的to_excel()函数将DataFrame导出为Excel文件。 ### 回答3: 要将DataFrame导出到Excel的其中一页,可以使用pandas库中的to_excel()方法。按照以下步骤进行操作: 1. 首先,确保你已经安装了最新版本的pandas库。你可以在命令提示符下使用以下命令来安装:pip install pandas 2. 导入pandas库,并根据需要导入其他所需的库。使用以下命令导入pandas库:import pandas as pd 3. 创建或加载你的DataFrame。假设你已经创建了一个名为df的DataFrame。 4. 调用DataFrame的to_excel()方法,将数据导出到Excel文件。to_excel()方法需要传入一个文件名和一个工作表名作为参数。你可以通过给这两个参数赋予不同的值来实现将DataFrame导出到Excel的其中一页。以下是一个例子: df.to_excel('文件名.xlsx', sheet_name='工作表名') 5. 运行程序后,Excel文件将被创建或更新,其中包含DataFrame的数据。你可以打开该文件,查看导出的数据。默认情况下,数据将从Excel的第一行开始导入。 请注意,使用to_excel()方法时,你可以使用多个参数来进行更多的自定义设置。例如,你可以指定导出数据的起始位置、是否包含DataFrame的索引等。这些参数可以根据你的需求进行调整。 这就是将DataFrame导出到Excel的其中一页的简单步骤。通过使用pandas的to_excel()方法,你可以轻松地将DataFrame中的数据导出到Excel文件的特定工作表中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值