力扣HOT100热题宝典--第1节

1、链表

148. 排序链表]

148. 排序链表 【难!!!】

【这道题需要掌握两种归并排序!!!归并(迭代、递归),快排】

大家都喜欢用的归并排序(事实上是对链表排序的最佳方案)

方式一 自顶向下的归并排序

时间复杂度 O(n log n)

空间复杂度: O(logn),其中 n是链表的长度。空间复杂度主要取决于递归调用的栈空间。【高度!!】

思路:归并排序(递归法)

找到链表中点,然后对左右两个链表进行排序
在这里插入图片描述

方式二 自底向上的归并排序

时间复杂度 O(n log n)

空间复杂度: O(1)
在这里插入图片描述

//自底向上方式
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null)
            return null;
        int len = 0;
        ListNode node = head;
        while (node != null) {
            node = node.next;
            len++;
        }
        ListNode dummyHead = new ListNode(0, head);
        for (int subLen = 1; subLen < len; subLen<<=1) {
            ListNode pre = dummyHead, cur = dummyHead.next;
            while (cur != null) {
                ListNode h1 = cur;
				// 
                for (int i = 1; i < subLen && cur.next != null; i++) {
                    cur = cur.next;
                }

                ListNode h2 = cur.next;
                cur.next = null;

                cur = h2;
                //cur遍历到第二段的最后一个节点
                for (int i = 1; i < subLen && cur!=null && cur.next != null; i++) {
                    cur = cur.next;
                }
                //第三段的开始节点
                ListNode next = null;
                if (cur != null) {
                    next = cur.next;
                    cur.next = null;
                }
                ListNode mergedHead = sortTwoList(h1, h2);
                //记录已排序分段的最后一个节点
                pre.next = mergedHead;
                while (pre.next != null) {
                    pre = pre.next;
                }
                cur = next;
            }
        }
        return dummyHead.next;
    }

    public ListNode sortTwoList(ListNode list1, ListNode list2) {
        ListNode dummyHead = new ListNode(0);
        ListNode node = dummyHead;

        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                node.next = list1;
                list1 = list1.next;
            } else {
                node.next = list2;
                list2 = list2.next;
            }
            node = node.next;
        }

            if (list1 != null) {
                node.next = list1;
            }

            if (list2 != null) {
                node.next = list2;
            }
        return dummyHead.next;
    }
}


class Solution {
    public ListNode sortList(ListNode head) {
        return sort(head,null);
    }
    //左闭右开
   public ListNode sort(ListNode start,ListNode end) {
        if(start==null) return start;
        if(start.next==end){
            start.next=null;
            return start;
        }
        ListNode slow=start;
        ListNode fast=start;
        // 寻找链表的中点
        while(fast!=end){
            slow=slow.next;
            fast=fast.next;
            if(fast!=end){
                fast=fast.next;
            }
        }
        ListNode mid=slow;
        ListNode list1=sort(start,mid);
        ListNode list2=sort(mid,end);
        return merge(list1,list2);
    }
    
	public ListNode sortTwoList(ListNode list1, ListNode list2) {
        ListNode dummyHead=new ListNode(0);
        ListNode node=dummyHead;
        while(list1!=null &&list2!=null){
            if(list1.val<=list2.val){
                node.next=list1;
                list1=list1.next;
                node=node.next;
            }else{
                node.next=list2;
                list2=list2.next;
                node=node.next;
            }
        }
        if(list1!=null)
            node.next=list1;
        if(list2!=null)
            node.next=list2;
        return dummyHead.next;
    }
}
114. 二叉树展开为链表

114. 二叉树展开为链表

原地算法(O(1) 额外空间)

方式二:

思路:对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为pre前驱节点,将当前节点的right节点赋给pre的right节点,然后将当前节点的right赋给next,并将当前节点的left设为空。对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束。

    public void flatten(TreeNode root) {
        TreeNode cur = root;
        while (cur != null) {
            TreeNode pre = cur.left;
            TreeNode next = pre;
            if (pre != null) {
                while (pre.right != null) {
                    pre = pre.right;
                }

                pre.right = cur.right;
                cur.left = null;
                cur.right = next;
            }
            cur = cur.right;
        }
    }


import java.util.*;
class Solution {
    public void flatten(TreeNode root) {
        ArrayList<TreeNode> list = new ArrayList<>();
        if(root==null) return;
        preOrer(list, root);
        TreeNode head = list.get(0);
        for (int i = 1; i < list.size(); i++) {
            head.left = null;
            head.right = list.get(i);
            head=head.right;
        }
    }

    public void preOrer(ArrayList<TreeNode> list, TreeNode root) {
        if (root != null) {
            list.add(root);
            preOrer(list, root.left);
            preOrer(list, root.right);
        }
    }
}

2、二分查找/排序

4. 寻找两个正序数组的中位数

4. 寻找两个正序数组的中位数 【难!!!】

算法的时间复杂度应该为 O(log (m+n))

https://www.geekxh.com/1.99.%E5%85%B6%E4%BB%96%E8%A1%A5%E5%85%85%E9%A2%98%E7%9B%AE/21.html#_03%E3%80%81%E8%AF%81%E6%98%8E%E8%BF%87%E7%A8%8B

小技巧,一般如果题目要求时间复杂度在O(log(n)),大部分都是可以使用二分的思想来进行求解

思路:

求出两个数组的总长度,找出中位数1和中位数2,相加做除就是题目的结果。

求中位数的方式比较特殊,是用删的方式,因为如果我们可以把多余的数排除掉,最终剩下的那个数,是不是就是我们要找的数

赋予最大值的意思只是说如果第一个数组的K/2不存在,则说明这个数组的长度小于K/2,那么另外一个数组的前K/2个我们是肯定不要的。

举个例子,加入第一个数组长度是2,第二个数组长度是12,则K为7,K/2为3,因为第一个数组长度小于3,则无法判断中位数是否在其中,而第二个数组的前3个肯定不是中位数!故当K/2不存在时,将其置为整数型最大值,这样就可以继续下一次循环。

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length;
        int len2 = nums2.length;
        int total = len1 + len2;
        int left = (total + 1) / 2;
        int right = (total + 2) / 2;
        return (findK(nums1, 0, nums2, 0, left) + findK(nums1, 0, nums2, 0, right)) / 2.0;
    }

    //找到两个数组中第k小的元素
    public int findK(int[] nums1, int i, int[] nums2, int j, int k) {
        //nums1数组为空
        if (i >= nums1.length)
            return nums2[j + k - 1];
        if (j >= nums2.length)
            return nums1[i + k - 1];
        // 排除到只剩两个元素取最小 即剩余元素的最小值
        if (k == 1) {
            return Math.min(nums1[i], nums2[j]);
        }
        //计算出每次要比较的两个数的值,来决定 "删除"" 哪边的元素
        int mid1 = (i + k / 2 - 1) < nums1.length ? nums1[i + k / 2 - 1] : Integer.MAX_VALUE;
        int mid2 = (j + k / 2 - 1) < nums2.length ? nums2[j + k / 2 - 1] : Integer.MAX_VALUE;
        //通过递归的方式,来模拟删除掉前K/2个元素
        if (mid1 < mid2) {
            return findK(nums1, i + k / 2, nums2, j, k - k / 2);
        }
        return findK(nums1, i, nums2, j + k / 2, k - k / 2);
    }
}
33. 搜索旋转排序数组

33. 搜索旋转排序数组

思想:二分法。

  1. 判断是否nums[mid] == target,成立,则返回下标;

  2. 否则,判断mid在左端,还是在右端。

  3. 接着判断target在mid的左侧还是右侧,来移动左右指针,找到mid

时间复杂度为 O(log n)

比如:总共有n个元素,每次查找的区间大小就是n,n/2,n/4,…,n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数。

由于n/2^ k取整后>=1,即令n/2^k=1, 可得k=log2n,(是以2为底,n的对数),所以时间复杂度可以表示O()=O(logn)

总结:

关于循环条件while (l <= r),何时用等号?

如果查找区间可以为0,那么加等号

class Solution {
public int search(int[] nums, int target) {
    int l = 0, r = nums.length - 1;
    while (l <= r) {
        int mid = l + (r-l)/2;
        if (nums[mid] == target) {
            return mid;
        }
        // 判断mid在左段,还是在右段
        if (nums[mid] >= nums[l]) {
            // 判断target在mid的左侧还是右侧
            if(target>=nums[l] && target<nums[mid])
                r=mid-1;
            else
                l=mid+1;
        } else {
            if(target>nums[mid] && target<=nums[r])
                l=mid+1;
            else
                r=mid-1;
        }
    }
    return -1;
}
}
34. 在排序数组中查找元素的第一个和最后一个位置

34. 在排序数组中查找元素的第一个和最后一个位置

思路:两次遍历找到最左边target的前一个数l和最右边target的后一个数r,然后计算target的个数是否大于0,如果大于零,那么返回结果new int[]{left+1,right-1};否则返回new int[]{-1,-1};

从左到右,left移到最右,

从右到左,right移到最左

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] <= target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        int r = left;

        left = 0;
        right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        int l = right;

        if (r - l - 1 > 0)
            return new int[]{l + 1, r - 1};
        else
            return new int[]{-1, -1};
    }
}
49. 字母异位词分组

49. 字母异位词分组

方法一:排序
字母相同,但排列不同的字符串,排序后都一定是相同的。因为每种字母的个数都是相同的,那么排序后的字符串就一定是相同的。

这里可以利用 stream 的 groupingBy 算子实现直接返回结果:

方式二:

遍历字符串数组,对字符串排序,将排序的字符串作为key,然后将所有的字符串保存到map中,最后直接取map.values()就是结果。

class Solution {
       public static List<List<String>> groupAnagrams(String[] strs) {
        List<List<String>> res = new ArrayList<>();
        HashMap<String, List<String>> map = new HashMap<>();
        for (String str : strs) {
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);
            //很有技巧
            List<String> list = map.getOrDefault(key, new ArrayList<>());
            list.add(str);
            map.put(key,list);
        }
           //注意这种写法!!
        return new ArrayList<>(map.values());
    }
}


class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        return new ArrayList<>(Arrays.stream(strs)
            .collect(Collectors.groupingBy(str -> {
                // 返回 str 排序后的结果。
                // 按排序后的结果来grouping by,算子类似于 sql 里的 group by。
                char[] array = str.toCharArray();
                Arrays.sort(array);
                return new String(array);
            })).values());
    }
}
75. 颜色分类

75. 颜色分类

时间复杂度O(n),空间复杂度O(1)

思路:

class Solution {
    public void sortColors(int[] nums) {
        int n=nums.length;
        // p0和p1指针的位置
        int p0=0;
        int p1=0;
        for(int i=0;i<n;i++){
            if(nums[i]==1){
                int tmp=nums[i];
                nums[i]=nums[p1];
                nums[p1]=tmp;
                p1++;
            }else if(nums[i]==0){
                int tmp=nums[i];
                nums[i]=nums[p0];
                nums[p0]=tmp;
                if(p0<p1){
                    tmp=nums[i];
                    nums[i]=nums[p1];
                    nums[p1]=tmp;
                }
                p0++;
                p1++;
            }
        }
    }
}


class Solution {
    public void sortColors(int[] nums) {
        int len = nums.length;
        // all in [0, zero) = 0
        // all in [zero, i) = 1
        // all in [two, len - 1] = 2
        int zero = 0;
        int two = len;
        int i = 0;
        while (i < two) {
            if (nums[i] == 0) {
                swap(nums, i, zero);
                zero++;
                i++;
            } else if (nums[i] == 1) {
                i++;
            } else {
                two--;
                swap(nums, i, two);
            }
        }
    }

    public void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}
347. 前 K 个高频元素

347. 前 K 个高频元素

【实现前k小和前k大!!】

时间复杂度 必须 优于 O(n log n)
在这里插入图片描述

方式一:堆排序

  • topk (前k大)用小根堆,维护堆大小不超过 k 即可。每次压入堆前和堆顶元素比较,如果比堆顶元素还小,直接扔掉,否则压入堆。检查堆大小是否超过 k,如果超过,弹出堆顶。复杂度是 nlogk
  • 避免使用大根堆,因为你得把所有元素压入堆,复杂度是 nlogn,而且还浪费内存。如果是海量元素,那就挂了。

[注意]

  • 求前 k 大,用小根堆,求前 k 小,用大根堆。

思路:

遍历数组,使用map记录元素对应的次数;

然后创建小顶堆,如果map中元素的频率大于最小堆中顶部的元素,则将顶部的元素删除并将该元素加入堆中,最后输出剩余的k个key就是结果。

时间复杂度:O(Nlogk),其中 N 为数组的长度。我们首先遍历原数组,并使用哈希表记录出现次数,每个元素需要 O(1) 的时间,共需 O(N) 的时间。随后,我们遍历「出现次数数组」,由于堆的大小至多为 k,因此每次堆操作需要O(logk) 的时间,共需 O(Nlogk) 的时间。二者之和为 O(Nlogk)。
空间复杂度:O(N)。哈希表的大小为O(N),而堆的大小为O(k),共计为 O(N)

初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。

topk 复杂度不是 klogk,是 nlogk.

  • 建堆,建堆复杂度是 n.
  • 插入,logn,上浮操作。
  • 删除(堆顶),一次 sink 操作,logn.
class Solution {
    public static int[] topKFrequent(int[] nums, int k) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        PriorityQueue<int[]> minHeap = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1];
            }
        });
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            int num = entry.getKey();
            int freq = entry.getValue();
            if (minHeap.size() == k) {
                if (minHeap.peek()[1] < freq) {
                    minHeap.poll();
                    minHeap.offer(new int[]{num, freq});
                }
            } else {
                minHeap.offer(new int[]{num, freq});
            }
        }

        int[] res = new int[k];
        for (
                int i = 0;
                i < k; i++) {
            res[i] = minHeap.poll()[0];
        }
        return res;
    }
}
406. 根据身高重建队列

406. 根据身高重建队列 【难!!】

【升序是默认排序,降序是需要指定的!!】

解释: 因为前面的每个人身高都比它大, 但是他前面只能有 K 个人大于等于它的身高,所以他只能放在第K个位置。

思路:身高从高到低排,再按k升序排

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        //注意数组的用法
        ArrayList<int[]> res = new ArrayList<>();
        
        Arrays.sort(people, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if (o1[0] == o2[0])
                    //升序
                    return o1[1] - o2[1];
                //降序
                return o2[0] - o1[0];
            }
        });
        for (int[] p : people) {
            res.add(p[1], p);
        }
        return res.toArray(new int[res.size()][]);
    }
}

整理不易🚀🚀,关注和收藏后拿走📌📌欢迎留言🧐👋📣
欢迎专注我的公众号AdaCoding 和 Github:AdaCoding123
在这里插入图片描述

  • 31
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值