力扣算法系统刷题题解记录一(数组、哈希、链表)

力扣算法系统刷题题解记录一(数组、链表、哈希表)

前言

参考顺序和资料:《代码随想录》
二刷要认真做笔记啦,加油!
笔记模板:

#### 解题思路
 #### 示意图
 #### 代码

一、数组

704.二分查找

日期:2023.4.12
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示意图:

在这里插入图片描述

解题思路

注意边界:左闭右闭 [left,right] while(left<=right) //右边界合法可以取到right
左闭右开 [left,right] while(left<right) //右边界不合法不可以取到right所以没有等号
1.设置左标记和右标记 左小于右 注意是下标
2.中间值: mid=(left+right)/2;或者 mid = (right-left)/2+left;
3.分别判断中间下标对应的值大于和小于目标值对应的左右标记的变化情况。
if(nums[mid]<target){ //注意是下标对应的值和目标值对比
left = mid +1; //左边闭的说明可以取到left,所以left=mid+1效率更高
if(nums[mid]>target){
right = mid - 1;//右边闭的说明可以取到right,所以left=mid-1效率更高
4.当left=right=mid,nums[mid]=target的时候就找到了target,,返回 mid,否则没有找到返回-1

代码
class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
       
        while(left<=right){
          int  mid=(left+right)/2;  //或者mid = ()
            if(nums[mid]<target){   //注意是下标对应的值和目标值对比
                left = mid +1;  //左边闭的说明可以取到left,所以left=mid+1效率更高
            }else if(nums[mid]>target){
                right = mid - 1;//右边闭的说明可以取到right,所以left=mid-1效率更高
            }else{
                return mid;
            }
        }
       return -1;
    }
}

未完待续

27.移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

你不需要考虑数组中超出新长度后面的元素。

示意图

在这里插入图片描述

解题思路

双指针思路时间复杂度O(n)
快指针 用于获取新数组中的元素
慢指针 获取新数组需要更新的位置
fast无论什么情况下都+1;slow在不等于要删的元素下才加一
时间复杂度:
O(n),其中 n 为序列的长度。我们只需要遍历该序列至多一次。

空间复杂度:
O(1)我们只需要常数的空间保存若干变量。

代码
class Solution {
    public int removeElement(int[] nums, int val) {
        int fast = 0;   //用于获取新数组中的元素
        int slow = 0;   //慢指针用于获取新数组所需要更新的位置
        for(fast = 0;fast<nums.length;fast++){
             if(nums[fast]!=val){
            nums[slow]=nums[fast];
            slow++;
            }
        }
       return slow; //返回数组的个数
    }
}

977. 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]

示例 2: 输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]

示意图

代码随想录上的思路

解题思路

双指针的写法
正数基本保持不变,变得是负数平方之后的数值

代码
class Solution {
    public int[] sortedSquares(int[] nums) {
        //双指针的写法
//      正数基本保持不变,变得是负数平方之后的数值
        int l = 0;  //左指针
        int r = nums.length-1;  //右指针
        int j = nums.length-1;  //新数组的长度
        int[] res = new int[nums.length];
        while(l<=r){
            if(nums[l]*nums[l]>nums[r]*nums[r]){
                res[j--]=nums[l]*nums[l++]; //后加加先用再加l
            }else{
                res[j--]=nums[r]*nums[r--];
            }
        }
        return res;
    }
}

209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示意图

在这里插入图片描述
重点部分:
在这里插入图片描述

解题思路

在本题中实现滑动窗口,主要确定如下三点:

窗口内是什么?
如何移动窗口的起始位置?
如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。

代码
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
       // 滑动窗口
        int left = 0;   // 滑动窗口起始位置
        int sum = 0;    // 滑动窗口的长度
        int result = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right]; 
            while (sum >= target) {
                result = Math.min(result, right - left + 1);
                sum -= nums[left++];    // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;     
    }
}

59. 螺旋矩阵 II

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示意图

在这里插入图片描述

解题思路

模拟顺时针画矩阵的过程:

填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上

时间复杂度 O(n^2): 模拟遍历二维矩阵的时间
空间复杂度 O(1)

代码
class Solution {
    public int[][] generateMatrix(int n) {
        int loop=0; //控制循环次数
        int[][] res = new int[n][n];    //螺旋矩阵是n*n的矩阵
        int start=0;    //每次循环的开始坐标是(start,start)
        int count=1;    //定义填充的数字
        int i,j;    //i行j列
        
        while(loop++<n/2){  //判断边界之后loop从1开始
//             模拟上面从左到右 行不变列变
            for(j=start;j<n-loop;j++){
                res[start][j]=count++;
            }
//             模拟右边从上到下 列不变行变
            for(i=start;i<n-loop;i++){
                res[i][j]=count++;
            }
//             模拟下边从右边到左边,行不变列变且递减
            for(;j>=loop;j--){
                res[i][j]=count++;
            }
//             模拟左边从下到上,列不变行变并且递减
            for(;i>=loop;i--){
                res[i][j]=count++;
            }
            start++;  //每转了1圈下一圈的坐标是上一圈的下一个位置
            
        }
        if(n%2==1){ // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
            res[start][start]=count;
        }
        return res;    
        
    }
}

203. 移除链表元素

题意:删除链表中等于给定值 val 的所有节点。

示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]

示例 2: 输入:head = [], val = 1 输出:[]

示意图

在这里插入图片描述
在这里插入图片描述

解题思路

推荐方法、添加虚拟结点的写法
用虚拟结点的好处就是不用再区分假如删除的是头结点,和假如删除的是非头结点

代码
/**
 * 添加虚节点方式
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
//添加虚拟结点的写法 用虚拟结点的好处就是不用再区分假如删除的是头结点,和假如删除的是非头结点
        if(head==null){
            return null;
        }
        ListNode dummy = new ListNode(-1,head); //定义虚拟结点
        ListNode pre = dummy;
        ListNode cur = head;
        while(cur!=null){
            if(cur.val == val){
                pre.next=cur.next;  //前一个结点指向当前结点的下一个结点
            }else{
                pre = cur;
            }
            cur = cur.next; //当前指针后移
        }
        return dummy.next;  //不能返回head,因为head可能被删了
    }
}

不添加虚拟节点方式

/**
 * 不添加虚拟节点方式
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
public ListNode removeElements(ListNode head, int val) {
    while (head != null && head.val == val) {
        head = head.next;
    }
    // 已经为null,提前退出
    if (head == null) {
        return head;
    }
    // 已确定当前head.val != val
    ListNode pre = head;
    ListNode cur = head.next;
    while (cur != null) {
        if (cur.val == val) {
            pre.next = cur.next;
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    return head;
}

不添加虚拟节点and pre Node方式

/**
 * 不添加虚拟节点and pre Node方式
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 * @param head
 * @param val
 * @return
 */
public ListNode removeElements(ListNode head, int val) {
    while(head!=null && head.val==val){
        head = head.next;
    }
    ListNode curr = head;
    while(curr!=null){
        while(curr.next!=null && curr.next.val == val){
            curr.next = curr.next.next;
        }
        curr = curr.next;
    }
    return head;
}

二、链表

链表和数组对比
在这里插入图片描述
添加节点:
在这里插入图片描述

删除节点:
在这里插入图片描述
代码表示:
java:

public class ListNode {
    // 结点的值
    int val;

    // 下一个结点
    ListNode next;

    // 节点的构造函数(无参)
    public ListNode() {
    }

    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }

    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

707.设计链表

在链表类中实现这些功能:

get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
在这里插入图片描述

示意图

删除链表节点:
在这里插入图片描述

添加链表节点:
在这里插入图片描述
这道题目设计链表的五个接口:

获取链表第index个节点的数值
在链表的最前面插入一个节点
在链表的最后面插入一个节点
在链表第index个节点前面插入一个节点
删除链表的第index个节点
可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目

链表操作的两种方式:

直接使用原来的链表来进行操作。
设置一个虚拟头结点在进行操作。
下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。

解题思路
代码
//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
class MyLinkedList {
    //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode currentNode = head;
        //包含一个虚拟头节点,所以查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }

    //在链表最前面插入一个节点,等价于在第0个元素前添加
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;
    }

    //删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        if (index == 0) {
            head = head.next;
	    return;
        }
        ListNode pred = head;
        for (int i = 0; i < index ; i++) {
            pred = pred.next;
        }
        pred.next = pred.next.next;
    }
}

//双链表
class ListNode{
    int val;
    ListNode next,prev;
    ListNode() {};
    ListNode(int val){
        this.val = val;
    }
}


class MyLinkedList {  

    //记录链表中元素的数量
    int size;
    //记录链表的虚拟头结点和尾结点
    ListNode head,tail;
    
    public MyLinkedList() {
        //初始化操作
        this.size = 0;
        this.head = new ListNode(0);
        this.tail = new ListNode(0);
        //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
        head.next=tail;
        tail.prev=head;
    }
    
    public int get(int index) {
        //判断index是否有效
        if(index<0 || index>=size){
            return -1;
        }
        ListNode cur = this.head;
        //判断是哪一边遍历时间更短
        if(index >= size / 2){
            //tail开始
            cur = tail;
            for(int i=0; i< size-index; i++){
                cur = cur.prev;
            }
        }else{
            for(int i=0; i<= index; i++){
                cur = cur.next; 
            }
        }
        return cur.val;
    }
    
    public void addAtHead(int val) {
        //等价于在第0个元素前添加
        addAtIndex(0,val);
    }
    
    public void addAtTail(int val) {
        //等价于在最后一个元素(null)前添加
        addAtIndex(size,val);
    }
    
    public void addAtIndex(int index, int val) {
        //index大于链表长度
        if(index>size){
            return;
        }
        //index小于0
        if(index<0){
            index = 0;
        }
        size++;
        //找到前驱
        ListNode pre = this.head;
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        //新建结点
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next.prev = newNode;
        newNode.prev = pre;
        pre.next = newNode;
        
    }
    
    public void deleteAtIndex(int index) {
        //判断索引是否有效
        if(index<0 || index>=size){
            return;
        }
        //删除操作
        size--;
        ListNode pre = this.head;
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        pre.next.next.prev = pre;
        pre.next = pre.next.next;
    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

206.反转链表

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

示意图

在这里插入图片描述

思路

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
遍历的终止条件:cur==null

其实只需要改变链表的next指针的指向:pre = cur->next,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
在这里插入图片描述

代码
// 双指针
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        ListNode temp = null;	//初始化结点
        while (cur != null) {
            temp = cur.next;// 保存下一个节点
            cur.next = prev;	//下一个结点指向前一个结点
            prev = cur;	//前一个结点后移
            cur = temp;	//当前结点后移
        }
        return prev;
    }
}
// 递归 
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev;
        }
        ListNode temp = null;
        temp = cur.next;// 先保存下一个节点
        cur.next = prev;// 反转
        // 更新prev、cur位置
        // prev = cur;
        // cur = temp;
        return reverse(cur, temp);
    }
}

24. 两两交换链表中的节点

输入:head = [1,2,3,4]
输出:[2,1,4,3]

思路

建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。画图!如下

示意图

在这里插入图片描述
操作之后
在这里插入图片描述

代码
```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 ListNode swapPairs(ListNode head) {
        ListNode dumyhead = new ListNode(-1);   //设置虚拟结点
        dumyhead.next = head;
        ListNode cur = dumyhead;
        ListNode temp;  //保存两个结点之后的结点
        ListNode temp1; //保存两个结点直接的第1个节点
        ListNode temp2; //保存两个结点直接的第2个节点
        while(cur.next != null && cur.next.next != null){
//             奇数个数和偶数个数的情况
            temp = cur.next.next.next;
            temp1 =  cur.next;
            temp2 = cur.next.next;
            cur.next = temp2;   //步骤1
           temp2.next = temp1; //步骤2
            temp1.next = temp; //步骤3
            cur = temp1;    // cur移动准备下一次交换
            
        }
        return dumyhead.next;
    }
}

19.删除链表的倒数第N个节点

题目:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
示例 1:
在这里插入图片描述

思路

注意:采用虚拟头结点,这样方便处理删除实际头结点的逻辑。
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾删掉slow所指向的节点就可以了,即slowIndex.next = slowIndex.next.next

示意图

1.定义fast指针和slow指针,初始值为虚拟头结点:

在这里插入图片描述
2.fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
在这里插入图片描述
3.fast和slow同时移动,直到fast指向末尾,如题:
在这里插入图片描述
4.删除slow指向的下一个节点,如图:在这里插入图片描述

代码
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyNode = new ListNode(0);
        dummyNode.next = head;  //定义虚拟结点
        // 两个快慢指针都指向虚拟结点
        ListNode fastIndex = dummyNode;
        ListNode slowIndex = dummyNode;
        // 只要快慢指针相差n+1个结点即可
        for(int i = 0;i<n;i++){
            fastIndex = fastIndex.next; //快指针向后移动
        }
        while(fastIndex.next != null){  //fastIndex移动直到为null
            fastIndex = fastIndex.next;
            slowIndex = slowIndex.next;
        }
        //此时 slowIndex 的位置就是待删除元素的前一个位置。
        //具体情况画一个链表长度为 3 的图来模拟代码来理解
        slowIndex.next = slowIndex.next.next;
        return dummyNode.next;
    }
}

142.环形链表II

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
在这里插入图片描述

思路

1.判断链表是否有环
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
在这里插入图片描述
fast和slow各自再走一步, fast和slow就相遇了。这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

示意图

在这里插入图片描述
2.寻找环入口
如果有环,如何找到这个环的入口?
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
在这里插入图片描述
那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z,

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
在这里插入图片描述
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

代码
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;   //定义快慢指针
        ListNode slow = head;   //定义快慢指针
        while(fast != null && fast.next != null){
            // 快指针先走,且每次走2个结点
            slow = slow.next;   //慢指针每次走一步
            fast = fast.next.next;  //快指针每次走2步
            if(slow == fast){   //有环
            // 两个指针,从头结点和相遇结点各走一步,直到相遇,相遇点就是环的入口
                ListNode index1 = fast; //相遇结点
                ListNode index2 = head; //头结点
                while(index1 != index2){
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1;
            }
           
        }
         return null;    //没有环就返回null
    }
}

三、哈希表Hash table

哈希表理论:
定义:哈希表就是根据关键码的值访问数据的数据结构。(数组就是一个哈希表)
在这里插入图片描述

使用场景:在需要快速判断一个数是否出现在一个集合中时。

哈希函数

把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
在这里插入图片描述
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来哈希碰撞登场

哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
在这里插入图片描述

一般哈希碰撞有两种解决方法, 拉链法和线性探测法。

#拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了

在这里插入图片描述

(数据规模是dataSize, 哈希表的大小为tableSize)

其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

#线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。

例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:

在这里插入图片描述

#常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
数组
set (集合)
map(映射)

这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
在这里插入图片描述

其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
在Java中,可以使用HashMap类或Hashtable类来实现哈希表。
HashMap:HashMap是Java集合框架中提供的一种哈希表实现。它使用键-值对(key-value pairs)的形式存储数据,并且允许使用null作为键和值。HashMap是非线程安全的,适用于单线程环境下的使用。
以下是使用HashMap的示例:

java
Copy code
import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建HashMap实例
        HashMap<String, Integer> hashMap = new HashMap<>();

        // 添加元素
        hashMap.put("apple", 1);
        hashMap.put("banana", 2);
        hashMap.put("orange", 3);

        // 获取元素
        int value = hashMap.get("banana");
        System.out.println(value);  // 输出: 2

        // 遍历哈希表
        for (String key : hashMap.keySet()) {
            int val = hashMap.get(key);
            System.out.println(key + ": " + val);
        }
    }
}

Hashtable:Hashtable是早期版本的哈希表实现,它与HashMap类似,但是是线程安全的。尽管Hashtable仍然可用,但通常推荐使用HashMap,因为HashMap在性能上更好,而且可以通过Collections.synchronizedMap方法来实现线程安全的HashMap。
以下是使用Hashtable的示例:

java
Copy code
import java.util.Hashtable;

public class HashtableExample {
    public static void main(String[] args) {
        // 创建Hashtable实例
        Hashtable<String, Integer> hashtable = new Hashtable<>();

        // 添加元素
        hashtable.put("apple", 1);
        hashtable.put("banana", 2);
        hashtable.put("orange", 3);

        // 获取元素
        int value = hashtable.get("banana");
        System.out.println(value);  // 输出: 2

        // 遍历哈希表
        for (String key : hashtable.keySet()) {
            int val = hashtable.get(key);
            System.out.println(key + ": " + val);
        }
    }
}

总结:Java中可以使用HashMap和Hashtable来实现哈希表。HashMap是非线程安全的,而Hashtable是线程安全的。在大多数情况下,建议使用HashMap,并根据需要进行线程同步处理。

定义:

HashSet<Integer> set = new HashSet<>();	//集合中存储的数组
HashSet<String> set = new HashSet<>();	//集合中存储的字符串

什么时候用Integer?
使用 int 类型表示整数时,适用于需要较高的内存和性能效率的场景,例如数值计算和循环索引
使用 Integer 类型表示整数时,适用于需要在对象上执行操作的场景,例如作为方法参数、存储在集合类中或进行比较和判等操作。它还提供了许多实用方法,并且允许使用 null 值表示缺失或未初始化状态。
注意的是,**自动装箱(Autoboxing)和拆箱(Unboxing)允许在 int 和 Integer 之间进行自动转换。**这意味着在大多数情况下,你可以根据需要选择使用 int 或 Integer 类型,它们之间会自动转换。

int a = 5;             // 原始类型 int
Integer b = Integer.valueOf(a);  // 自动装箱
int c = b.intValue();  // 自动拆箱

242. 有效的字母异位词

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true

解题思路

需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。
为了方便举例,判断一下字符串s= “aee”, t = “eae”。
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。

示意图

在这里插入图片描述

代码
/**
 * 242. 有效的字母异位词 字典解法
 * 时间复杂度O(m+n) 空间复杂度O(1)
 */
class Solution {
    public boolean isAnagram(String s, String t) {
        int[] record = new int[26];

        for (int i = 0; i < s.length(); i++) {
            record[s.charAt(i) - 'a']++;     // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
        }

        for (int i = 0; i < t.length(); i++) {
            record[t.charAt(i) - 'a']--;
        }
        
        for (int count: record) {
            if (count != 0) {               // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
                return false;
            }
        }
        return true;                        // record数组所有元素都为零0,说明字符串s和t是字母异位词
    }
}

349.两数交集

题意:给定两个数组,编写一个函数来计算它们的交集。
在这里插入图片描述

解题思路

方法一、暴力解法:
1.建立新 num;
2.对nums1 , nums2排序(方便查找);
3.求长度记录为l1 l2 ,另外对nums2新建一个p,用于记下下标(通过不断更新p对j赋值,减少遍历次数)
4.for循环对nums1遍历
5.判断nums1中前后两数字是否相等,如果相等,不用对num2遍历;
6.else 对num2进行遍历,找到相同值就push进num,记录下一次遍历的下表数,break退出本层循环;
7.返回num;

方法二、注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序

这道题用暴力的解法时间复杂度是O(n^2),可以使用哈希法进一步优化。
java中可以使用hashSet,c++中用unordered_set读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复。
在Java中,HashSet是一种实现了Set接口的集合类,它使用哈希表来存储数据,不允许重复元素,并且不保证元素的顺序。HashSet提供了高效的插入、删除和查找操作,适用于需要存储唯一元素的场景。

示意图

在这里插入图片描述

代码

C++暴力:

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        vector<int> num;
        int l1=nums1.size(),l2=nums2.size(),p=0;
        sort(nums1.begin(),nums1.end());
        sort(nums2.begin(),nums2.end());
        for(int i=0;i<l1;i++){
            if(i&&nums1[i]==nums1[i-1])
                i+=0;
            else{
              for(int j=p;j<l2;j++)
                if(nums1[i]==nums2[j]){
                    num.push_back(nums1[i]);
                    p=j;
                    break;
                }
            }
      }
      return num;
    }
};

java哈希:

 public int[] intersection(int[] nums1, int[] nums2) {
        // 用于存放nums1数组的不重复的所有数字
        HashSet<Integer> set = new HashSet<>();
        // 遍历添加到set
        for (int i = 0; i < nums1.length; i++) {
            set.add(nums1[i]);	//遍历nums1数组的元素放到集合set中
        }
        // 用于存放交集的数字
        HashSet<Integer> list = new HashSet<>();
        for (int i = 0; i < nums2.length; i++) {
            // 如果包含就添加到set中去
            if (set.contains(nums2[i])) {
                list.add(nums2[i]);
            }
        }
        // 方法一: 用JDK8Stream流新特性,不过耗时会更长
//        return list.stream().mapToInt(x->x).toArray();
        // 方法二: 用数组自己再次遍历添加,迭代器遍历HashSet (推荐)
        int[] result = new int[list.size()];
        int  i = 0;
        for (Integer integer : list) {
            result[i++] = integer;
        }
        return result;
    }

1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

解题思路

方法一:暴力解法
return new int[]{i,j};或者直接返回数组下标(需要新建一个)
方法二:哈希表
1.使用 HashMap 来存储已遍历的元素及其索引。这样可以将时间复杂度从 O(n^2) 降低到 O(n),提高算法的效率。
2.遍历数组时,判断目标元素与当前元素的差是否已经存在于 HashMap 中**(target-x)**。如果存在,则返回对应的索引。
3.如果不存在,则将当前元素及其索引添加到 HashMap 中。

示意图
代码

暴力:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];	/定义存放9
        for(int i = 0;i<nums.length;i++){
            for(int j = i+1;j<nums.length;j++){	//注意这里不是j=0!而是j=i+1,不然下一次循环重复遍历了数据。
                if(nums[i]+nums[j]==target){
                    res[0]=i;
                    res[1]=j;	
                    //return new int[]{i,j};或者直接返回数组下标
                }
            }
        }
        return res;
    }
}

哈希表:

import java.util.HashMap;
import java.util.Map;

public class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();

        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                return new int[]{map.get(complement), i};
            }
            map.put(nums[i], i);
        }

        throw new IllegalArgumentException("No two sum solution");
    }
}

在优化后的代码中,我们使用了 HashMap 来存储已经遍历过的元素及其索引。在遍历数组时,我们计算当前元素与目标元素的差值 complement。然后检查 complement 是否已存在于 HashMap 中,如果存在,则返回对应的索引。如果不存在,则将当前元素及其索引添加到 HashMap 中。如果没有找到符合条件的索引组合,则抛出 IllegalArgumentException 异常。
代码随想录的写法:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2]; //定义存放结果的数组
        if(nums == null || nums.length==0){
            return res;
        }
        Map<Integer,Integer> map = new HashMap<>();    //定义哈希表
        for(int i = 0;i<nums.length;i++){
            int temp = target - nums[i];    //遍历当前元素,并在map中寻找是否有匹配的key
            if(map.containsKey(temp)){	//检查 map 是否包含键为 temp 的元素,即是否存在一个之前遍历过的元素与当前元素之和等于目标值。
                res[1]=i;
                res[0]=map.get(temp);
                break;
            }
            map.put(nums[i],i); //如果没有找到匹配的对,就把访问过的元素和下标加入到map中
        }
        return res;
    }
}

在这段代码中,map 的定义和使用是一种典型的 “两遍扫描” 策略。在第一遍扫描时,虽然 map 中还没有存储键和值,但它的目的是为了记录已经遍历过的元素和它们的索引。

具体来说,代码中的逻辑如下:

首先,检查输入的 nums 数组是否为空或长度为 0。如果是,则直接返回初始状态的 res 数组。

创建一个空的 HashMap 对象 map,用于存储元素和它们的索引。

开始遍历 nums 数组,对于每个元素 nums[i],执行以下操作:

计算目标值与当前元素的差值,并将结果存储在变量 temp 中:int temp = target - nums[i];
**检查 map 是否包含键为 temp 的元素,即是否存在一个之前遍历过的元素与当前元素之和等于目标值。**如果存在,则说明找到了满足条件的两个元素,更新 res 数组的值,并通过 break 终止循环。
如果不存在匹配的键,则将当前元素 nums[i] 和对应的索引 i 存储在 map 中,以便后续的遍历能够找到与之匹配的元素。
在循环结束后,如果没有找到匹配的元素,res 数组仍然保持初始状态,即两个元素的下标为 0。

因此,在代码的逻辑中,**map.containsKey(temp) 的判断是为了在遍历到当前元素时,检查之前是否已经存在与之匹配的元素。**如果存在,就可以立即找到满足条件的结果,而无需再进行后续的遍历。这也是该算法的优化点之一,通过使用 map 存储已遍历的元素和索引,可以将查找匹配元素的时间复杂度从 O(n) 降低到 O(1)。

202.快乐数

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。

如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:

输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

解题思路

哈希法使用场景:当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
题目中提到可能是无限循环 但始终变不到 1,所以也就是说sum会重复出现,因此需要用哈希表判断sum是否出现了多次。

示意图

不用

代码
class Solution { // 定义一个名为Solution的类
    public boolean isHappy(int n) { // 定义一个名为isHappy的公共方法,该方法以整数n作为输入并返回布尔值
        Set<Integer> record = new HashSet<>(); // 创建一个名为record的新HashSet来存储整数
        while (n != 1 && !record.contains(n)) { // 当n不等于1且record不包含n时
            record.add(n); // 将n添加到record中
            n = getNextNumber(n); // 将n设置为调用getNextNumber并以n作为输入的结果
        }
        return n == 1; // 如果n等于1,则返回true,否则返回false
    }

    private int getNextNumber(int n) { // 定义一个名为getNextNumber的私有方法,该方法以整数n作为输入并返回一个整数
        int res = 0; // 将一个名为res的新整数变量初始化为0
        while (n > 0) { // 当n大于0时
            int temp = n % 10; // 将temp设置为n除以10的余数
            res += temp * temp; // 将temp的平方加到res中
            n = n / 10; // 将n设置为n除以10
        }
        return res; // 返回res
    }
}

时间复杂度分析:这段代码的时间复杂度是O(log n)。isHappy方法中的while循环最多执行log n次,因为getNextNumber方法将n减少到1的时间复杂度为O(log n)。因此,isHappy方法的时间复杂度为O(log n)。getNextNumber方法中的while循环最多执行log n次,因为n的位数最多为log n。因此,getNextNumber方法的时间复杂度为O(log n)。由于isHappy方法和getNextNumber方法的时间复杂度都是O(log n),因此整个程序的时间复杂度也是O(log n)。

454.四数相加

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

解题思路

本题是使用哈希法的经典题目,而0015.三数之和 (opens new window),0018.四数之和 (opens new window)并不合适使用哈希法,因为三数之和和四数之和这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。
而这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!
如果本题想难度升级:就是给出一个数组(而不是四个数组),在这里找出四个元素相加等于0,答案中不可以包含重复的四元组,大家可以思考一下,后续的文章我也会讲到的。
本题解题步骤:
C++:
首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
最后返回统计值 count 就可以了
JAVA:
这段代码的思路可以分为以下几个步骤:

  • 创建一个变量 res 来记录相加为0的情况的个数,并初始化为0。 创建一个 HashMap map
  • 用于存储元素之和及其出现次数的映射关系。
  • 遍历数组 nums1 中的每个元素 i,以及数组 nums2 中的每个元素 j: 计算 i 和 j 的和,并将结果存储在变量 sum 中。
  • 使用 map.getOrDefault(sum, 0) 方法来获取 sum 在 HashMap 中对应的值,如果不存在则默认为0。 将 sum 的值加1,并使用 map.put(sum, …) 方法将更新后的值存储回HashMap。
  • 再次遍历数组 nums3 中的每个元素 i,以及数组 nums4 中的每个元素 j: 计算 0 - i - j的值,即相加为0所需要的补数。
  • 使用 map.getOrDefault(…) 方法来获取补数在 HashMap 中对应的值,即相加为0的情况的个数。 将得到的个数累加到变量 res 中。 返回变量 res,即相加为0的情况的总个数。

总体而言,该算法通过使用哈希表来存储部分数组元素之和及其出现次数的信息,以降低时间复杂度。通过两次遍历四个数组,分别统计两两组合的元素之和,然后在哈希表中查找补数的个数,最终得到相加为0的情况的个数。

相关知识点

1.map.getOrDefault(…)
map.getOrDefault(…)是 Java 中 HashMap 类提供的一个方法。它用于获取指定键的对应值,如果键不存在于 HashMap 中,则返回一个默认值。

该方法的语法如下:

getOrDefault(Object key, V defaultValue)

其中:

key 是要获取值的键。
defaultValue 是默认值,如果指定的键不存在于 HashMap 中,则返回该默认值。
如果指定的键存在于 HashMap 中,则返回与该键关联的值;如果键不存在,则返回默认值。
在给定的代码中,map.getOrDefault(0 - i - j, 0) 表示在 map 中查找键为 0 - i - j 的值,如果不存在则返回默认值 0。

2.HashMap和HashSet
HashMap和HashSet是Java集合框架中的两种不同类型的数据结构。它们的定义和使用场景如下:

HashMap(哈希映射):

定义:HashMap是基于哈希表实现的键值对存储结构。它允许使用不同的键和值类型,并且键不允许重复。
使用场景:HashMap常用于需要根据键来查找、插入或删除值的情况。它适用于需要通过键快速查找值的场景,例如索引、缓存、字典等。
HashSet(哈希集合):

定义:HashSet是基于哈希表实现的集合,它存储独特的元素,不允许重复。它没有键值对的概念,只关注元素的唯一性
使用场景:HashSet适用于需要存储唯一元素并且不关心顺序的情况。它通常用于去重操作,例如去除列表中的重复元素,或者判断某个元素是否存在于集合中。
区别:

  • 存储方式:HashMap存储键值对,而HashSet仅存储元素。
  • 数据访问:在HashMap中,通过键来访问值;在HashSet中,通过元素本身来判断唯一性和访问。
  • 元素唯一性:HashMap的键是唯一的,不允许重复;HashSet中的元素也是唯一的,不允许重复。
  • 迭代顺序:HashMap中的元素没有固定的顺序;HashSet中的元素也没有固定的顺序
  • 存储性能:HashMap对键进行哈希计算,可以快速查找、插入和删除;HashSet对元素进行哈希计算,也具备快速的查找、插入和删除特性。
    总的来说,HashMap适用于键值对的存储和查找,而HashSet适用于元素的唯一性判断和集合操作。
区别HashMapHashSet
存储方式存储键值对仅仅存储元素
数据访问通过键来访问数据访问
元素唯一性键是唯一值是唯一
迭代顺序无序无序
存储性能对键进行哈希计算,可以快速查找、插入和删除对元素进行哈希计算,可以快速查找、插入和删除
代码
class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        int res = 0; // 用于记录相加为0的情况的个数
        Map<Integer, Integer> map = new HashMap<Integer, Integer>(); // 用于存储元素之和及其出现次数的映射
        // 统计nums1和nums2中的元素之和,同时统计出现的次数,放入map
        for (int i : nums1) {
            for (int j : nums2) {
                int sum = i + j; // 计算nums1和nums2中元素之和
                map.put(sum, map.getOrDefault(sum, 0) + 1); // 将元素之和及其出现次数放入map,如果已存在则取出当前值并加1,不存在则默认为0并加1
            }
        }
        // 统计nums3和nums4中的元素之和,在map中找是否存在相加为0的情况,同时记录次数
        for (int i : nums3) {
            for (int j : nums4) {
                res += map.getOrDefault(0 - i - j, 0); // 在map中查找相加为0的情况的次数,如果不存在则默认为0
            }
        }
        return res; // 返回相加为0的情况的个数
    }
}

383.赎金信

给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。

(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

注意:

你可以假设两个字符串均只含有小写字母。

canConstruct(“a”, “b”) -> false
canConstruct(“aa”, “ab”) -> false
canConstruct(“aa”, “aab”) -> true

思路

这道题目和242.有效的字母异位词很像,242.有效的字母异位词 (opens new window)相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求 字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。

  • 第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用

  • 第二点 “你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要。

  • 因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。

然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。依然是数组在哈希法中的应用。用数组干啥,都用map完事了,其实在本题的情况下,**使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!**数据量大的话就能体现出来差别了。 所以数组更加简单直接有效。

代码

暴力解法

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        for (int i = 0; i < magazine.length(); i++) {
            for (int j = 0; j < ransomNote.length(); j++) {
                // 在ransomNote中找到和magazine相同的字符
                if (magazine[i] == ransomNote[j]) {
                    ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符
                    break;
                }
            }
        }
        // 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
        if (ransomNote.length() == 0) {
            return true;
        }
        return false;
    }
};

哈希表
注意因为是判断A中的字符是否可以用B中的字符组成,所以要先遍历B中的,也就是说B中的字符可以比A中的多。B中的字符用来加,A中的字符用来减。

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        int[] record = new int[26]; // 哈希表,用于记录字符出现的次数,索引0表示字符'a',索引1表示字符'b',以此类推
        if (ransomNote.length() > magazine.length()) return false; // 如果ransomNote的长度大于magazine的长度,无法构建,直接返回false
        for (char c : magazine.toCharArray()) {
            record[c - 'a'] += 1; // 统计magazine中字符出现的次数,对应的哈希表值加1
        }
        for (char c : ransomNote.toCharArray()) {
            record[c - 'a'] -= 1; // 统计ransomNote中字符出现的次数,对应的哈希表值减1
        }
        for (int i : record) {
            if (i < 0) { // 如果哈希表中存在负数,说明ransomNote中存在magazine中没有的字符,无法构建,返回false
                return false;
            }
        }
        return true; // 哈希表中所有值均为非负数,说明ransomNote可以由magazine中的字符构建,返回true
    }
}


15.三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。

示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]

解题思路

其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。

而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。

接下来我来介绍另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些,那么来讲解一下具体实现的思路。

示意图

在这里插入图片描述

拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。

依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。

接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>(); // 存储结果的列表

        Arrays.sort(nums); // 对数组进行排序

        // 找出满足 a + b + c = 0 的三元组
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.length; i++) {
            // 如果排序后的第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) {
                return result;
            }

            if (i > 0 && nums[i] == nums[i - 1]) { // 去重a,如果当前元素与前一个元素相同,跳过当前元素
                continue;
            }

            int left = i + 1; // 左指针,指向当前元素的下一个元素
            int right = nums.length - 1; // 右指针,指向数组末尾元素
            while (right > left) {
                int sum = nums[i] + nums[left] + nums[right]; // 当前三个元素的和
                if (sum > 0) { // 如果和大于零,需要减小和,移动右指针向左
                    right--;
                } else if (sum < 0) { // 如果和小于零,需要增大和,移动左指针向右
                    left++;
                } else { // 和等于零,找到一个满足条件的三元组
                    result.add(Arrays.asList(nums[i], nums[left], nums[right])); // 添加到结果列表中

                    // 去重逻辑应该放在找到一个三元组之后,对b和c进行去重
                    while (right > left && nums[right] == nums[right - 1]) right--; // 去重右指针
                    while (right > left && nums[left] == nums[left + 1]) left++; // 去重左指针

                    right--; // 继续向内移动指针,寻找下一个可能的三元组
                    left++;
                }
            }
        }
        return result; // 返回结果列表
    }
}

解惑:
为什么是nums[left] == nums[left + 1] 而不是nums[left] == nums[left -1]?

在代码中,nums[left] == nums[left + 1]用于判断左指针指向的元素是否与下一个元素重复,进而进行去重操作。
这是因为在排序后的数组中,如果存在重复的元素,它们会相邻地排列在一起。当找到一个满足条件的三元组后,为了避免重复计算相同的三元组,需要跳过重复的元素。
**当nums[left] == nums[left + 1]成立时,说明左指针指向的元素与下一个元素相同,存在重复。**在这种情况下,移动左指针到下一个不重复的元素,即left++。这样可以确保不重复地考虑相同值的元素。
相反,nums[left] == nums[left - 1]是不正确的,因为左指针指向的是当前元素,而不是前一个元素。在这种情况下,如果使用nums[left] == nums[left - 1],将无法正确地判断当前元素是否与前一个元素重复,从而无法进行正确的去重操作。

为什么 nums[right] == nums[right - 1]?
在代码中,nums[right] == nums[right - 1]用于判断右指针指向的元素是否与前一个元素重复,以进行去重操作。
当找到一个满足条件的三元组后,为了避免重复计算相同的三元组,需要跳过重复的元素。在排序后的数组中,如果存在重复的元素,它们会相邻地排列在一起。
当nums[right] == nums[right - 1]成立时,说明右指针指向的元素与前一个元素相同,存在重复。在这种情况下,移动右指针到下一个不重复的元素,即right–。这样可以确保不重复地考虑相同值的元素。
通过判断右指针指向的元素与前一个元素是否相同,可以有效地去除重复的三元组。

为什么nums[i] == nums[i - 1] ,但nums[j] == nums[j+1]?

在这段代码中,i 是用于遍历数组 nums 的索引变量,而 j 是内部循环中的指针变量。
nums[i] == nums[i - 1]:这个判断语句用于去重 a 的操作,确保每个 a 的值都是唯一的。当 nums[i] 与前一个元素 nums[i - 1] 相同时,说明当前的 a 值已经被处理过了,为了避免重复计算,我们可以直接跳过当前的 nums[i],进入下一次迭代。
nums[j] == nums[j+1]:这个判断语句用于去重 b 和 c 的操作,确保每个 b 和 c 的值都是唯一的。当 nums[j] 与后一个元素 nums[j+1] 相同时,说明当前的 b 值已经被处理过了,为了避免重复计算,我们可以直接跳过当前的 nums[j],进入下一次迭代。
这两个判断语句都是用于去除重复的情况,确保每个元素只被考虑一次,从而避免生成重复的三元组。

18.四数之和

题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]

解题思路

四数之和,和15.三数之和 (opens new window)是一个思路,都是使用双指针法, 基本解法就是在15.三数之和 (opens new window)的基础上再套一层for循环。

但是有一些细节需要注意,例如: 不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。

15.三数之和 (opens new window)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] == 0。

四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。

那么一样的道理,五数之和、六数之和等等都采用这种解法。

对于15.三数之和 (opens new window)双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。

之前我们讲过哈希表的经典题目:454.四数相加II (opens new window),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。

而454.四数相加II (opens new window)是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少!

示意图

代码
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> result = new ArrayList<>();  // 用于存储结果的列表
        Arrays.sort(nums);  // 对数组进行排序,方便后续的去重和判断大小

        for (int i = 0; i < nums.length; i++) {  // 遍历数组
            // 如果当前元素大于0且大于目标值,则直接返回结果,进行剪枝操作
            if (nums[i] > 0 && nums[i] > target) {
                return result;
            }

            if (i > 0 && nums[i - 1] == nums[i]) {  // 对当前元素进行去重判断
                continue;
            }

            for (int j = i + 1; j < nums.length; j++) {  // 再次遍历数组,从当前元素的下一个位置开始
                if (j > i + 1 && nums[j - 1] == nums[j]) {  // 对当前元素进行去重判断
                    continue;
                }

                int left = j + 1;  // 左指针
                int right = nums.length - 1;  // 右指针
                while (right > left) {  // 使用双指针法查找满足条件的四个数之和
                    long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];  // 四个数之和,使用long类型防止溢出
                    if (sum > target) {  // 如果和大于目标值,移动右指针向左
                        right--;
                    } else if (sum < target) {  // 如果和小于目标值,移动左指针向右
                        left++;
                    } else {  // 如果和等于目标值,将结果加入到列表中,并进行去重判断
                        result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        // 对左右指针所指的元素进行去重判断
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left + 1]) left++;

                        left++;  // 移动左指针向右
                        right--;  // 移动右指针向左
                    }
                }
            }
        }
        return result;  // 返回最终结果
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微莱羽墨

感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值