LeetCode刷题笔记

LeetCode刷题笔记!
摘要由CSDN通过智能技术生成

1、在二分法中,遇到了寻找mid时的小问题,要用

mid = left + (right - left) / 2;

代替

mid = (left + right) / 2;

为什么呢?在测试的数据中会有left + right超越int边界的情况采用上面的写法就可以避免很多;

二分法的约束条件

...
	int left = 0, right = nums.length - 1;
	while(left <= right) {
   
		int mid = left + (right - left) / 2;
		if(nums[mid] == target) return mid;
		else if(nums[mid] < target) {
   
			left = mid + 1;
		} else {
   
			right = mid - 1;
		}
	}
...

为什么while循环的条件是 <= , 而不是 <?
答:初始化的right的赋值是nums.length - 1, 即最后的一个元素的索引,此时的<=相当于两端区间是闭区间[left, right]。如果条件为left < right,则区间相当于[left, right),需要配合初始值right = nums.length,因为索引nums.length在数组nums[]中是越界的。

2、下一个排列

对于长度为n的数组,找到其下一个更大的排序,若无更大排序则变为最小排序。例:[1, 2, 3]下一个排序为[1, 3 ,2];
直接上算法:

  1. 从后向前找到满足a[i] < a[i+1]的较小数a[i],此时[i+1,n)必然是下降序列;
  2. 如果找到顺序对,那么在[i+1,n)从后向前找第一个a[i] < a[j],a[j]为较大数;
  3. 交换a[i]与a[j],此时[i+1,n)必为降序,再反转区间[i+1,n),使其变为升序,则可得到下一个稍大的排序了;

算法大概书写:

public Solution{
   
	public void nextPermutation(int[] nums) {
   
        int firstIndex = -1;        
        for(int i = nums.length - 2; i >= 0; i--) {
   
            if(nums[i] < nums[i + 1]) {
   
                firstIndex = i;
                break;
            }
        }
        if(firstIndex == -1) {
   
            reverse(nums, 0, nums.length - 1);
            return;
        }

        int secondIndex = -1;
        for(int i = nums.length - 1; i >= firstIndex; i--) {
   
             if(nums[i] > nums[firstIndex]){
   
                 secondIndex = i;
                 break;
             }
        }

        swap(nums, firstIndex, secondIndex);
        reverse(nums, firstIndex + 1, nums.length - 1);
    }                       
	 //交换数组中两元素
	 public void swap(int[] nums, int i, int j){
   ...}
	 //通过swap,逆置数组区间
	 public void reverse(int[] nums, int i, int j){
   
	 	while(i < j){
   
            swap(nums, i++, j--);
        }
	 }
}

3、排序链表

时间复杂度O(n * log n) 的排序算法有归并排序、堆排序、快速排序(最差时间复杂度为O(n^2)),其中最合适链表的是归并排序
归并排序基于分治算法,最容易想到的实现方法是自顶向下,考虑到递归调用栈的空间,空间复杂度为O(log n )。如果要达到O(1)的空间复杂度,需要使用自底向上的实现方法。
方法一:自顶向下归(真头假尾)
1.找到链表中点(通过快慢指针),拆分为两个子链表,再分别进行排序;
2. 将拆分的子链表进行两两合并,得到完整排序后的链表;

Class Solution {
   
		//主函数, dummyTail传入空;
		public ListNode sortList(ListNode head) {
   
        	return sortList(head, null);
    }

		//head为含有元素的链表头节点,dummyTail为空尾指针
	    public ListNode sortList(ListNode head, ListNode dummyTail) {
   
            if(head == null) {
   
                return head;
            }
			//为了甩掉空尾指针,当传入两个节点的情况,分离节点为单独节点
            if(head.next == dummyTail) {
   
                head.next = null;
                return head;
            }
            
			//快慢指针寻找链表中点,通过快指针fast、fast.next判断边界;
            ListNode slow = head, fast = head;
            while(fast != dummyTail && fast.next != dummyTail) {
   
                slow = slow.next;
                fast = fast.next.next;
            }

            ListNode mid = slow;
            ListNode list1 = sortList(head, mid);
            ListNode list2 = sortList(mid, dummyTail);
            
            ListNode sorted = merge(list1, list2);
            return sorted;
    }
    
    //merge即为经典的两有序链表排序融合为同一链表
	public ListNode merge(ListNode head1, ListNode head2) {
   
        ListNode dummyHead = new ListNode(0);
        ListNode tmp = dummyHead, tmp1 = head1, tmp2 = head2;
        while(tmp1 != null && tmp2 != null) {
   
            if(tmp1.val <= tmp2.val) {
   
                tmp.next = tmp1;
                tmp1 = tmp1.next;
            } else {
   
                tmp.next = tmp2;
                tmp2 = tmp2.next;
            }
            tmp = tmp.next;
        }

        if(tmp1 != null) {
   
            tmp.next = tmp1;
        }
        if(tmp2 != null) {
   
            tmp.next = tmp2;
        }

        return dummyHead.next;
    }
}

4. 删除排序链表中的重复元素

给定有序链表, 链表中重复元素的出现位置是连续的,删除重复的元素包括元素本身. 例:1, 2, 3, 3 -> 1, 2;

//一次遍历
class Solution {
   
    public ListNode deleteDuplicates(ListNode head) {
   
        if(head == null) {
   
        	return null;
        }
        //这里使用了假头部节点,为了方便删除头部节点后返回结果dummyHead.next;
        ListNode dummyHead = new ListNode(0, head);
        ListNode cur = dummyHead;
        //当前节点从假头节点开始遍历,循环的判定是cur的后续两节点是否不为空,当有一个为空时便不用进行去重操作;
        while(cur.next != null && cur.next.next != null){
   
            if(cur.next.val == cur.next.next.val) {
   
                int tmp = cur.next.val;
                while(cur.next != null && cur.next.val == tmp) {
   
                    cur.next = cur.next.next;
                }
            } else {
   
                cur = cur.next;
            }
        }

        return dummyHead.next;
    }
}

5.最小覆盖字串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:

对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
在这里插入图片描述
题解:滑动窗口
通过两个数组have[128],need[128]记录窗口内的字符出现频数和需要的字符串t的每个字符频数,因为ASCII码一共有128位,所以数组的大小设定为128,且可以通过have[(char)asc2]形式存储频数
窗口的边界采用左闭右开[left, right), 方便记录窗口长度right - left;

当右边界不超出字符串时循环:
首先移动右窗口right,判断新收纳进窗口的字符s.charAt(right)need[]数组中的频数need[s.charAt(right)]是否为0,如果为0则continue,继续扩展right;如果不为0,说明是需要的字符,用count记录窗口内有需要字符的总频数,当且仅当已有字符串目标字符出现的次数小于目标字符串字符的出现次数时,count才会+1,以此保证count不是被同一字符多余的频次给加满的;
之后循环判断此时的count是否已经满足需要的字符总频次,满足的时候记录此时的字串长度,若为当前最短则记录。之后开始移动左窗口,当左边界字符不为需要的字符时移动左窗口;若左边界字符为需要的字符时,总频次减一、已有字符频次减一,之后移动左边界;

最后返回字串。

class Solution {
   
    public String minWindow(String s, String t) {
   
        if (s == null || s == "" || t == null || t == "" || s.length() < t.length()) {
   
            return "";
        }
        //维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
        //ASCII表总长128
        int[] need = new int[128];
        int[] have = new int[128];

        //将目标字符串指定字符的出现次数记录
        for (int i = 0; i < t.length(); i++) {
   
            need[t.charAt(i)]++;
        }

        //分别为左指针,右指针,最小长度(初始值为一定不可达到的长度)
        //已有字符串中目标字符串指定字符的出现总频次以及最小覆盖子串在原字符串中的起始位置
        int left = 0, right = 0, min = s.length() + 1, count = 0, start = 0;
        while (right < s.length()) {
   
            char r = s.charAt(right);
            //说明该字符不被目标字符串需要,此时有两种情况
            // 1.循环刚开始,那么直接移动右指针即可,不需要做多余判断
            // 2.循环已经开始一段时间,此处又有两种情况
            //  2.1 上一次条件不满足,已有字符串指定字符出现次数不满足目标字符串指定字符出现次数,那么此时
            //      如果该字符还不被目标字符串需要,就不需要进行多余判断,右指针移动即可
            //  2.2 左指针已经移动完毕,那么此时就相当于循环刚开始,同理直接移动右指针
            if (need[r] == 0) {
   
                right++;
                continue;
            }
            //当且仅当已有字符串目标字符出现的次数小于目标字符串字符的出现次数时,count才会+1
            //是为了后续能直接判断已有字符串是否已经包含了目标字符串的所有字符,不需要挨个比对字符出现的次数
            if (have[r] < need[r]) {
   
                count++;
            }
            //已有字符串中目标字符出现的次数+1
            have[r]++;
            //移动右指针
            right++;
            //当且仅当已有字符串已经包含了所有目标字符串的字符,且出现频次一定大于或等于指定频次
            while (count == t.length()) {
   
                //挡窗口的长度比已有的最短值小时,更改最小值,并记录起始位置
                if (right - left < min) {
   
                    min = right - left;
                    start = left;
                }
                char l = s.charAt(left);
                //如果左边即将要去掉的字符不被目标字符串需要,那么不需要多余判断,直接可以移动左指针
                if (need[l] == 0) {
   
                    left++;
                    continue;
                }
                //如果左边即将要去掉的字符被目标字符串需要,且出现的频次正好等于指定频次,那么如果去掉了这个字符,
                //就不满足覆盖子串的条件,此时要破坏循环条件跳出循环,即控制目标字符串指定字符的出现总频次(count)-1
                if (have[l] == need[l]) {
   
                    count--;
                }
                //已有字符串中目标字符出现的次数-1
                have[l]--;
                //移动左指针
                left++;
            }
        }
        //如果最小长度还为初始值,说明没有符合条件的子串
        if (min == s.length() + 1) {
   
            return "";
        }
        //返回的为以记录的起始位置为起点,记录的最短长度为距离的指定字符串中截取的子串
        return s.substring(start, start + min);
    }
}

6. 从前序与中序遍历序列构造二叉树

给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。(preorder 和 inorder 均无重复元素)

在这里插入图片描述
题解:

  1. 递归
    通过前序遍历的头元素得到树的根节点,再根据中序遍历头节点的位置,将头节点左右的元素划分为左右子树。之后再分别从前序遍历中继续寻找左右子树的头节点,递归往复…
public TreeNode buildTree(int[] preorder, int[] inorder) {
   
    return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length);
}

private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) {
   
    // preorder 为空,直接返回 null
    if (p_start == p_end) {
   
        return null;
    }
    int root_val = preorder[p_start];
    TreeNode root = new TreeNode(root_val);
    
    //在中序遍历中找到根节点的位置,这里可以通过Hash表改进
    //可改进//
    int i_root_index = 0;
    for (int i = i_start; i < i_end; i++) {
   
        if (root_val == inorder[i]) {
   
            i_root_index = i;
            break;
        }
    }
    //
    
    int leftNum = i_root_index - i_start;
    //递归的构造左子树
    root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index);
    //递归的构造右子树
    root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end);
    return root;
}

上述代码,每一次在中序遍历中寻找根节点都需要一次遍历,这里可以通过Hash表改进,因为题中规定树各元素的值是唯一的,所以可以通过值直接获取元素在中序遍历中的位置。

//通过HashMap改进的代码
public TreeNode buildTree(int[] preorder, int[] inorder) {
   
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < inorder.length; i++) {
   
        map.put(inorder[i], i);
    }
    return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length, map);
}

private TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end,
                                 HashMap<Integer, Integer> map) {
   
    if (p_start == p_end) {
   
        return null;
    }
    int root_val = preorder[p_start];
    TreeNode root = new TreeNode(root_val);
    int i_root_index = map.get(root_val);
    int leftNum = i_root_index - i_start;
    root.left = buildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index, map);
    root.right = buildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end, map);
    return root;
}

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

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
在这里插入图片描述
题解1:归并
将两数组归并为同一数组后取中位数。
题解2:二分法
寻找上下两数组的相对中位线,使得线左侧全部元素小于线右侧全部元素。
两数组总数分别为m、n,当我们用中线将其分隔(中位数紧邻线左侧,中线下标为中线右侧元素,即 i 为中线位下标,中线左侧元素为i - 1,右侧元素为 i ),无论奇偶线左侧元素总数应为(m + n + 1)/ 2(向上取整)。
在这里插入图片描述

这里我们定位初始值left = 0, right = m,区间为左闭右开[left, right),循环判断边界left < right,我们先通过二分法,快速锁定第一行数组中线,根据中线左侧元素总数totalLeft固定,便得知第二数组中线的位置。之后进行判定,第一行数组中线左侧第一个元素 nums1[i - 1]是否小于第二行中线右侧第一个元素nums2[j]。若不小于则不满足中线的定义要求,需要向左二分,将第一行数组右区间赋值中线左侧位置right = i - 1,下一步寻找[left, i - 1);若小于则说明满足,但需要继续向右二分,为了寻找是否还有比当前中线左侧数值更大并满足要求的中线,将第一行数组左区间赋值中线右一元素i,left = i;
在这里插入图片描述
最后要判断一下极端情况,第一行数组中线左侧无值
在这里插入图片描述
要设定第一行数组左侧最大元素为极小值;
第一行数组中线右侧无值
在这里插入图片描述

要设定第一行数组中线右侧最小值为极大值;
第二数组中线左侧无值
在这里插入图片描述
要设定第二行数组中线左侧数值为极小值;
第二行数组中线右侧无值
在这里插入图片描述
要设定第二行数组中线右侧数值为极大值;

最后根据奇偶,有两种返回情况;上下数组中线两侧都有可能是中位数,当总数为奇数时,选上下数组中线左侧最大的元素即为中位数;当总数为偶数时,(选中线左侧最大 + 中线右侧最小) / 2 即为中位数;

public class Solution {
   

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
   
        if (nums1.length > nums2.length) {
   
            int[] temp = nums1;
            nums1 = nums2;
            nums2 = temp;
        }

        int m = nums1.length;
        int n = nums2.length;

        // 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
        int totalLeft = (m + n + 1) / 2;

        // 在 nums1 的区间 [0, m] 里查找恰当的分割线,
        // 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
        int left = 0;
        int right = m;

        while (left < right) {
   
            int i = left + (right - left + 1) / 2;
            int j = totalLeft - i;
            if (nums1[i - 1] > nums2[j]) {
   
                // 下一轮搜索的区间 [left, i - 1]
                right = i - 1;
            } else {
   
                // 下一轮搜索的区间 [i, right]
                left = i;
            }
        }

        int i = left;
        int j = totalLeft - i;

        int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        int nums1RightMin = i == m ? Integer.MAX_VALUE : nums1[i];
        int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        int nums2RightMin = j == n ? Integer.MAX_VALUE : nums2[j];

        if (((m + n) % 2) == 1) {
   
            return Math.max(nums1LeftMax, nums2LeftMax);
        } else {
   
            return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
        }
    }
}

8.求根节点到叶节点数字之和

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。

叶节点 是指没有子节点的节点。

在这里插入图片描述
题解:
1、深度优先遍历
从根节点向下依次判断,如果为叶子节点则直接加到数字之和,如果不是,则计算子节点对应的数字;
在这里插入图片描述

class Solution {
   
    public int sumNumbers(TreeNode root) {
   
    //初始总和传入0
        return helper(root, 0);
    }

    public int helper(TreeNode root, int prevSum) {
   
    //访问为空节点返回0值
        if(root == null) {
   
            return 0;
        }
	//计算当前总和
        int sum = prevSum * 10 + root.val;
	//若当前访问的为叶子节点直接将当前总和返回
        if(root.left == null && root.right == null) {
   
            return sum;
        }
	//如不为叶子节点返回遍历左右子树的总和
        return helper(root.left, sum) + helper(root.right, sum);
    }
    
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值