📌力扣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. 二叉树展开为链表
原地算法(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))
小技巧,一般如果题目要求时间复杂度在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. 搜索旋转排序数组
思想:二分法。
-
判断是否nums[mid] == target,成立,则返回下标;
-
否则,判断mid在左端,还是在右端。
-
接着判断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. 在排序数组中查找元素的第一个和最后一个位置
思路:两次遍历找到最左边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. 字母异位词分组
方法一:排序
字母相同,但排列不同的字符串,排序后都一定是相同的。因为每种字母的个数都是相同的,那么排序后的字符串就一定是相同的。
这里可以利用 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. 颜色分类
时间复杂度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 个高频元素
【实现前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