1.160.相交链表
(1)暴力解法
循环遍历listA的所有节点,循环内遍历B所有节点,检查当前遍历到的的A、B中的节点是否一致。
如果一致,标记,跳出循环。
最后根据标记为返回结果。
时间复杂度O(len(A)*len(B))
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
boolean res = false;
while(headA != null){
ListNode tempB = headB;
while(tempB != null){
if(headA == tempB){
res = true;
break;
}
tempB = tempB.next;
}
if(res){
break;
}
headA = headA.next;
}
if(res) return headA;
return null;
}
}
(2)双指针法
双指针法利用的是对称性。
从对称性角度来理解这种解法:A链表长a+c,B链表长b+c。想要让两个head走相同的步数,并且同时到达相交节点,只要headA再走b步,headB再走A步。那么只需要headA挪到原来的headB处,headB挪到原来的headA处。
检查,当没有交点的时候,都走m+n时,会都为null。
最坏时间复杂度O(len(A) + len(B))
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode tempA = headA , tempB = headB;
while(tempA != tempB){
if(tempA == null){
tempA = headB;
}else{
tempA = tempA.next;
}
if(tempB == null){
tempB = headA;
}else{
tempB = tempB.next;
}
}
return tempA;
}
}
(3)哈希法
时间复杂度O(len(A) + len(B))
空间复杂度O(len(A))
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
while(headA!= null){
set.add(headA);
headA = headA.next;
}
while(headB!=null){
if(set.contains(headB)){
return headB;
}else{
headB = headB.next;
}
}
return null;
}
}
(1)递归思想
思路:使用递归实现二叉树的从下向上的搜索(后序遍历)
递归三部曲:
a.确定参数及返回值
b.确定终止条件
这里的终止条件是遍历到的结点如果是p或者是q或者是null,就返回该节点
c.确定单层的逻辑
单层逻辑这里包含着最近公共祖先的判断逻辑:接到左右子树分别递归回来的返回结果。如果左、右均不为空,说明该节点就是最近的公共祖先(依据后序遍历的顺序)。如果不满足上面的条件,该节点需要继续把子结点的结果传上去,返回不为空的那个结果(都为空就返回null)
java
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left != null && right != null) return root;
else if (left != null && right == null) return left;
else if (left == null && right != null) return right;
else return null;
}
}
3.回文链表
(1)复制到数组中用双指针法(下面是复制到了ArrayList中)
空间复杂度O(n),时间复杂度O(n)
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
boolean flag = true;
List<Integer> list = new ArrayList<>();
while(head != null){
list.add(head.val);
head = head.next;
}
int low = 0 , high = list.size() - 1;
while(low < high){
if(list.get(low) != list.get(high)){
flag = false;
}
low++;
high--;
}
return flag;
}
}
(2)希望避免O(n)的空间复杂度,我们直接修改链表的结构,但这也会造成并发时需要加锁的问题:
(1)暴力搜索,但是会时间超限,时间复杂度O(n^2)
java
public int[] dailyTemperatures(int[] temperatures) {
int[] res = new int[temperatures.length];
for(int i = 0 ; i < temperatures.length ; i++){
for(int j = i+1 ; j < temperatures.length ;j++){
if(temperatures[j] > temperatures[i]){
res[i] = j - i;
break;
}
}
}
return res;
}
(2)这属于"下一个更大"的问题,可以考虑使用单调栈,用空间换取时间。
我们需要记忆的是目前还无法确定的,维护一个从栈底到栈顶单调递减的栈,直到下一个元素大于栈内,把栈内所有满足小于该元素的成员弹出,并得到这些成员的结果。然后该元素入栈,重复上述行为。
元素的大小可以通过temperatures数组获取,我们要维护的是元素的下标。
单调栈的思想通常用于寻找"下一个更大"或者"下一个更小"的问题,核心在于用空间换取时间,实现一次遍历,记录下目前还不能确定答案,能在未来确定答案的元素。
java
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int[] res = new int[temperatures.length];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(0);
for(int index = 1 ; index < temperatures.length ; index++){
while(!stack.isEmpty() && temperatures[index] > temperatures[stack.peek()]){
res[stack.peek()] = index - stack.peek();
stack.pop();
}
stack.push(index);
}
return res;
}
}
总结起来,单调栈的应用场景就是:在O(n)的时间内实现寻找下一个更大或下一个更小。
二叉树有天然的递归结构。
翻转二叉树,按照先序或者后序遍历的顺序翻转均可。
中序遍历翻转的话会有问题,它不是按照递归的顺序翻转的。
可以分解一下这道题,使解法更加清晰
(1)大框架是遍历,preOrder或者postOrder
(2)order的时候要做reverse,交换左右节点的位置
java
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
preOrder(root);
return root;
}
public void preOrder(TreeNode root){
if(root == null) return;
reverse(root);//先序遍历,本层逻辑
preOrder(root.left);
preOrder(root.right);
}
public void reverse(TreeNode root){
if(root == null) return;
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
}
(1)暴力搜索
java
class Solution {
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length;
int cols = matrix[0].length;
for(int i = 0 ; i < rows; i++){
for(int j = 0 ;j < cols; j++){
if(matrix[i][j] == '1'){
maxSide = Math.max(maxSide,1);
int currentPossibleMaxside = Math.min(rows - i , cols - j );
for(int k = 1 ; k < currentPossibleMaxside ; k++){
boolean flag = true;
for(int m = 0 ; m <= k; m++){
if(matrix[i + k][j + m] == '0' || matrix[i + m][j + k] =='0'){
flag = false;
break;
}
}
if(flag == false){
break;
}else{
maxSide = Math.max(maxSide,k+1);
}
}
}
}
}
return maxSide*maxSide;
}
}
(2)这种暴力搜索问题,通常会考虑记忆化降低时间复杂度,进而归结到动态规划。
动态规划五部曲:
-
确定dp数组及下标含义
-
确定递推方程
-
确定如何初始化
-
确定递推顺序
-
dp模拟(dp模拟要做的就是打印前几步dp数组,看看和自己预想的一样不一样,如果不一样再找问题)
a. dp数组的含义是和递推顺序也有关系的。我们按照从上到下,从左到右的顺序去遍历二维矩阵。dp[i][j]定义为:以下标i,j为右下角,所能构成的满足题目要求的最大正方形的边长。
b.递推方程。递推方程要分类讨论,结合当前访问的元素的情况;同时也与递推顺序有关,递推方程是原先"记忆化"精练出来的,体现了原来"记忆化"的痕迹。
如果当前位置内容是'0',那么以当前位置为右下角,不可能构成满足题意的正方形,dp[i][j] = 0.
如果当前位置是'1':
java
class Solution {
public int maximalSquare(char[][] matrix) {
int[][] dp = new int[matrix.length][matrix[0].length];
for(int i = 0 ; i < matrix.length ; i++){
if(matrix[i][0] == '1'){
dp[i][0] = 1;
}
}
for(int j = 0 ; j < matrix[0].length ; j++){
if(matrix[0][j] == '1'){
dp[0][j] = 1;
}
}
for(int i = 1 ; i < matrix.length ; i++){
for(int j = 1 ; j < matrix[0].length ; j++){
if(matrix[i][j] == '0'){
dp[i][j] = 0;
}else{
dp[i][j] = Math.min(dp[i-1][j],Math.min(dp[i][j-1],dp[i-1][j-1])) + 1;
}
}
}
int maxSide = 0 ;
for(int i = 0 ; i < matrix.length ; i++){
for(int j = 0 ;j < matrix[0].length ; j++){
maxSide = Math.max(maxSide,dp[i][j]);
}
}
return maxSide*maxSide;
}
}
时间复杂度O(m*n),空间复杂度O(m*n)
类似题目:1277.统计全为1的正方形个数
这道题用的dp递推方程和最大正方形的递推方程一样。原因是dp[i][j]既然表示了以i,j下标元素为右下角的最大正方形边长,那么它也就表示了以i,j下标元素为右下角的正方形个数。
最后把dp[i][j]累加起来即为答案。
这里如果按照之前单独初始化的方式,需要考虑许多边界条件(因为涉及ans的累加),这里把初始化的逻辑放在了遍历计算dp里。
java
public int countSquares(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
int[][] dp = new int[rows][cols];
int ans = 0 ;
for(int i = 0 ; i < rows ; i++){
for(int j = 0 ; j < cols ; j++){
if(i == 0 || j == 0){
dp[i][j] = matrix[i][j];
}else if(matrix[i][j] == 0){
dp[i][j] = 0;
}else{
dp[i][j] = Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1]) + 1;
}
ans += dp[i][j];
}
}
return ans;
}
(1)暴力解法
先排序,后返回
平均时间复杂度O(nlogn),不符合题目要求
空间复杂度O(1)
(2)用空间换取时间,把遍历数组的过程看成是读入数据的过程,在这期间维护一个堆。
TopK问题,考虑使用堆作为数据结构,本题中维护一个大小为k的最小堆,读取完数据后在堆顶的即为第k大的数据。
时间复杂度和空间复杂度是由堆这种数据结构的性质决定的。
时间复杂度O(nlogk)
空间复杂度O(k)
java
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>((n1,n2) ->n1-n2);
for (int num : nums) {
heap.add(num);
if(heap.size() > k){
heap.poll();
}
}
return heap.poll();
}
}
(3)快速选择算法
其核心思想是分治,使用递归实现。
(可以证明,)平均情况下,快速选择算法可以实现O(n)的时间复杂度,最坏情况为O(n^2)
快速排序模板:
java
public static void main(String[] args) {
int[] nums= {3,2,3,1,2,4,5,5,6};
quickSort(nums,0,nums.length-1);
for (int num : nums) {
System.out.println(num);
}
}
public static void quickSort(int[] nums,int l , int h){
if(l >= h) return ;
int p = partition2(nums,l,h);
quickSort(nums,l,p-1);
quickSort(nums,p+1,h);
}
public static int partition2(int[] nums,int l , int h){
int i = l, j = h;
int pivot = nums[i];
while(i < j){
while(i < j && nums[j] > pivot) j--;
while(i < j && nums[i] <= pivot) i++;
swap(nums,i,j);
}
swap(nums,l,j); //此时i和j重合
return j;//此时i和j重合
}
public static void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
应用到快速选择解决topK问题上:
java
class Solution {
public int findKthLargest(int[] nums, int k) {
k = nums.length - k + 1;
return quickSort3(nums,0,nums.length-1,k);
}
public static int quickSort3(int[] nums,int l , int h , int k){
if(l >= h) return nums[l];
int p = partition2(nums,l,h);
//下标为p处,前面有p个元素
if(p == k-1) return nums[p];
else if(p > k-1) return quickSort3(nums,l,p-1,k);
else return quickSort3(nums,p+1,h,k);
}
public static int partition2(int[] nums,int l , int h){
int i = l, j = h;
int pivot = nums[i];
while(i < j){
while(i < j && nums[j] > pivot) j--;
while(i < j && nums[i] <= pivot) i++;
swap(nums,i,j);
}
swap(nums,l,j); //此时i和j重合
return j;//此时i和j重合
}
public static void swap(int[] nums,int i , int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
前缀树是一种非典型多叉树,适合用于保存需要频繁查询,以及频繁查询前缀的若干字符串。
前缀树的结点设计是一个字母表加上isEnd标志位。
对于sea,sells,she三个字符串,存储类似于:
前缀树的结构是固定的,节点是否为空代表该字母在该位置是否存储在了前缀树里。
java
package org.example.quickSort;
public class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (char c : word.toCharArray()) {
int index = c - 'a';
if(node.children[index] == null){
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = this;
for (char c : word.toCharArray()) {
int index = c - 'a';
if(node.children[index] == null){
return false;
}
node = node.children[index];
}
return node.isEnd;
}
public boolean startsWith(String prefix) {
Trie node = this;
for (char c : prefix.toCharArray()) {
int index = c - 'a';
if(node.children[index] == null){
return false;
}
node = node.children[index];
}
return true;
}
}
9.207.课程表
这道题根据题面描述就是一道拓扑排序问题
拓扑排序判断结果是否包含所有courses
拓扑排序解法过程要注意一下几点:
a.记录inDegree(通过数组)
b.记录当前节点指向的下一节点都有哪些 (通过map)
c.需要一个队列来维护已知的待处理的 目前入度为0的节点,结果通过一个List返回
java
class Solution {
public static boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer> res = new ArrayList<>();
int[] inDegree = new int[numCourses];
Map<Integer,List<Integer>> nextCourses = new HashMap<>();
Deque<Integer> queue = new ArrayDeque<>();
for(int i = 0 ; i < numCourses ; i++){
nextCourses.put(i,new ArrayList<>());
}
for(int i = 0 ; i < prerequisites.length ; i++){
int fromNode = prerequisites[i][0];
int toNode = prerequisites[i][1];
nextCourses.get(fromNode).add(toNode);
inDegree[toNode]++;
}
for(int i = 0 ; i < numCourses ; i++){
if(inDegree[i] == 0){
queue.add(i);
}
}
while(!queue.isEmpty()){
int node = queue.poll();
res.add(node);
//删除该节点的所有指向其他节点的入度
List<Integer> list = nextCourses.get(node);
for (Integer i : list) {
inDegree[i]--;
if(inDegree[i] == 0){
queue.add(i);
}
}
}
if(res.size() == numCourses){
return true;
}
return false;
}
}
10.206.反转链表
考查对链表这种数据结构的操作能力。
java
public ListNode reverseList(ListNode head){
ListNode prev = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}