代码随想录

133 阅读17分钟

⭐ACM格式输入输出⭐

输入:数字、字符、字符串 间隔:空格、换行、符号

  • 数字——int——单独读取,以空格分开nextInt();——int等基本数据类型的数组,同行或不同都可以

  • 字符——char

  • 字符串——String——单独读取用next(),以空格划分

  • *next()读取单独字符串,到空格停止,在读取输入后将光标放在同一行中。

  • *nextLine()读取整行为字符串,到回车停止,在读取输入后将光标放在下一行。

  • split(",");按逗号分割单行内容,一般都是存到数组中

  • 如果多行输入

    • 循环调用scanne.nextLine();获取每行
    • scanne.nextLine().split(",");来分隔单行中的内容
  • 单行很长输入

    • scanne.nextLine()获取改行,再层层分割。例如
String[] s = sc.nextLine().split(",");

for (int i = 0; i < s.length; ++i) {
    String[] ss = s[i].split(":");
    int a = Integer.parseInt(ss[0]);
    int b = Integer.parseInt(ss[1]);
}

数组-二分查找

⭐特征:有序数组+无重复元素

基础二分

image.png

搜索插入位置

同二分,多一个返回插入索引

image.png

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

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]

示例 3:

输入: nums = [], target = 0
输出: [-1,-1]

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

双指针法

情况:1数组为空。2目标在数组左。3目标在数组右。4目标在数组区间内,但不在数组中。5在数组中

数组为空、目标在数组左,目标在数组右、直接返回【-1,-1】

左右指针定义为初值为-1,因为左右指针,分别从左往右/从右往左遍历时。如果没有目标值,即情况4。不会修改指针。定义为[-1,-1]后可以直接返回 。

image.png

两次二分,找到目标后继续往左往右二分查找,看是否有左右边界。

注意数组声明:

int[] array = new int[2];
int[] array = new int[]{1,2,3,4};
int m = 1, n = 2;
int[] array = new int[]{m,n};

image.png

27. 移除元素

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

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

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

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

 

示例 1:

输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2]
解释: 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3]
解释: 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

只需要记录重复个数返回,同时修改nums数组即可,会自动根据返回的答案,输出移除后数组。不用重新创建数组。

⭐容器遍历——foreach

简洁,但没有索引不能操作元素

for (类型 X :容器对象) {
    语句,要用到X;
}

image.png

977有序数组的平方

创立数组 int[] array = nwe int[length];

数组排序:直接调用Array.sort(要排序数组)

熟悉排序方法

image.png

链表

  1. 声明链表(头节点)对象!!
  2. 头节点可能被删除,用新对象指向原链表head
  3. 操作链表,再声明一个节点变量来操作

⭐if判断,需要加else,不能直接写。会报空指针异常。

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //声明链表(头节点)对象!!
        ListNode pre = new ListNode(0);
        //头节点可能被删除,用新对象指向原链表head
        pre.next = head;
        //操作链表,再声明一个节点变量来操作
        ListNode temp = pre ;

        while (temp.next != null) {
            if (temp.next.val == val) {
                temp.next = temp.next.next;
            }
            //相等跳过,不等往后顺延。
            //不加这个,会原地一直while
            else {temp = temp.next;}
        }
    return pre.next;
    }
}

设计链表

  • //注意
  • //插入索引节点前,操作(索引节点的)上一个节点的next指向插入节点。所以循环i不能等于index
  • //删除索引节点,操作(索引节点的)上一个节点的next指向下一个节点。所以循环i不能等于index
  • //两个类————链表节点类ListNode和链表类LinkedList
  • //初始化空链表,直接赋值/new,不用在声明变量
  • //获取or返回链表要声明新的头节点,#指向#链表头节点
  • //操作处理链表,声明新指针节点=head。

//链表类
class MyLinkedList {

    //属性1:头节点,指向链表第一个节点
    ListNode head;
    //属性2:长度,get时需要
    int size;

    //无参构造方法,初始化一个空链表
    public MyLinkedList() {
        
        //头节点,val为0
        head = new ListNode(0);
        //长为0,空链表
        size = 0;

    }
    
    
    public int get(int index) {
        //int n = index;
        if (index < 0 || index > size - 1) {
            return -1;
        }

        //处理链表,都声明一个节点类型的指向链表的指针
        ListNode cur = head;

        for (int i = 0; i <= index; i++) {
            cur = cur.next;
        }

        return cur.val;

    }
    
    //头插法
    public void addAtHead(int val) {

        //ListNode ah = new ListNode(val);
        //ah.next = head.next;
        //head.next = ah;
        addAtIndex(0, val);

    }
    
    public void addAtTail(int val) {

        addAtIndex(size, val);
        

    }
    
    //插入索引位置节点前.
    //如下标为0-1,size=2,index为2,插入最后
    public void addAtIndex(int index, int val) {

        if (index < 0 || index > size) {
            return;
        }                                      

        size++;
        ListNode cur = head;
        for (int i = 0; i < index; i++) {
            cur= cur.next;
        }

        ListNode toadd = new ListNode(val);
        //cur.next = toadd;
        //toadd.next = cur.next.next;注意顺序,这样插入后面链表断了
        toadd.next = cur.next;
        cur.next = toadd;

    }
                                            
    public void deleteAtIndex(int index) {

        if (index < 0 || index > size - 1) {
            return;
        }

        size--;
        ListNode cur = head;
        //把上一个节点指向下一个节点,索引从上一个节点开始操作。不能等于index
        for (int i = 0; i < index; i++) {
            cur= cur.next;
        }

        cur.next = cur.next.next;

    }
}

//节点类
class ListNode{
    int val;
    ListNode next;
    ListNode(){};
    ListNode(int val){
        this.val = val;
    }
    ListNode(int val, ListNode next){
        this.val = val;
        this.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);
 */

反转链表

迭代

遍历期间,改变指向

链表分头指针和头节点。头节点就是链表第一个节点,存第一个数据。

链表中操作或者声明的pre,head,cur,cur.next等,都是节点

  • “=”符号是赋值(等号右表赋值给左边),根据顺序不同,可以是赋值,可以是指向。
  • head = cur.next——头是cur的下一个节点
  • cur.next = head——cur的下一个节点,指向头

注意:

  • while循环条件:cur != null,让最后一个节点也参与遍历
    • 如果是cur.next != null,遍历不到最后一个
  • cur指针遍历期间,要对链表.next(指向)进行操作,要声明一个临时节点,⭐保存节点.next⭐。
    • 否则next改变,无法继续遍历
/**
 * 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; }
 * }
 */

 //head就是链表第一个节点
class Solution {
    public ListNode reverseList(ListNode head) {

        ListNode pre = null;
        ListNode cur = head;
        while (cur != null) {

            //既要把cur.next指向pre,指向后,cur链表断连,但还要把cur往右移动
            //取一个临时节点,做值传递

            //记录cur遍历的下一个节点
            ListNode temp = cur.next;
            //cur指向pre
            cur.next = pre;

            //头插,pre和cur都右移一位
            pre = cur;
            cur = temp;

        }
        return pre;
    }
}

递归

关键:cur。

  • 不要理解为返回cur这个节点,要理解为返回的是已经反转过的部分的头指针,而恰巧这个头指针是确定的。
  • 递到最后一个节点,归的时候,每层改变已经反转部分的末尾,指向该层的前一个节点。同时层层返回翻转后的头节点。
//递归——方法中调用方法
class Solution {
    public ListNode reverseList(ListNode head) {

        if (head == null || head.next == null) {
            return head;
        }

        //调用递归,反转好后面的链表
        //用一个节点接收递归返回值
        //递归到最后一层后head=5,返回第二层,此时head=4
        ListNode cur = reverseList(head.next);

        //让5指向4
        head.next.next = head;
        //此时4还指向5防止成环,4引用置空
        head.next = null;

        return cur;

    }
}

删除倒数第n个节点

核心:根据倒数n,找到正序索引位置

常规思路

遍历,得到链表长度L 再次遍历,到L-n+1节点时,该是要删除节点

要点:

  • 声明一个标识节点,指向头。结尾返回标识.next。即原链表
  • 栈的声明
    • Deque<ListNode> stack = new LinkedList<ListNode>();
  • 声明指针处理链表节点
    • ListNode cur = res; 指针指向标识节点
ListNode dummy = new ListNode();
        dummy.next = head;
  
或者
ListNode res = new ListNode(); 
res.next = head;


class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //初始化哑节点,作为返回标记,且不用特殊判断

        ListNode res = new ListNode();
        res.next = head;
        //初始化一个链表构成的空栈
        //声明元素格式为节点的链表对象
        //用元素格式为节点的Deque双向队列【容器】接收
        Deque<ListNode> stack = new LinkedList<ListNode>();

        //初始化指针节点,处理链表
        ListNode cur = res;
        //遍历链表,压栈
        while(cur != null){
            stack.push(cur);
            cur = cur.next;
        }
        //链表入完栈,弹栈弹出n个,栈顶即为前驱节点。
        for(int i = 0; i < n ;i++ ) {

            stack.pop();
        }
        ListNode pre = stack.peek();
        pre.next = pre.next.next;

        return res.next;
    }
}
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //初始化哑节点,作为返回标记,且不用特殊判断
        ListNode dummy = new ListNode( 0 , head);
        //初始化一个链表构成的空栈
        //声明元素格式为节点的链表对象
        //用元素格式为节点的Deque双向队列【容器】接收
        Deque<ListNode> stack = new LinkedList<ListNode>();

        //初始化指针节点,处理链表
        ListNode cur = dummy;
        //遍历链表,压栈
        while(cur != null){
            stack.push(cur);
            cur = cur.next;
        }
        //链表入完栈,弹栈弹出n个,栈顶即为前驱节点。
        for(int i = 0; i < n ;i++ ) {

            stack.pop();
        }
        ListNode pre = stack.peek();
        pre.next = pre.next.next;

        return dummy.next;
    }
}

快慢双指针,指针相隔n

快慢指针初始为0/1。快指针先遍历n,之后同时遍历。快指针指向空时,慢指针即为要删除节点。

 //用双指针
/*倒数2——删除5
1   2   3   4   5   6   null
l       r
    l       r
            l       r
*/

//dummy——head
//  |      |
//慢指针——快指针

//dummy节点,获取链表备份,用来返回。dummy指向head
//快指针,单纯移动。可以取在原链表=head
//慢指针,要删除节点。取在备份链表上=dummy

//快比慢提前n个节点,间隔n-1个节点

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        
        ListNode dummy = new ListNode();
        ListNode left = new ListNode();
        ListNode right = new ListNode();
        dummy.next = head;
        right = head;
        left = dummy;

        for(int i = 0; i < n; i++) {
            right = right.next;
        }
        while(right != null) {
            left = left.next;
            right = right.next;
        }
        left.next = left.next.next;

        return dummy.next;
    }
}

代码如下

核心:

  • 声明哑节点指向链表,让后续操作实现在链表上
  • 快指针初始在1,慢指针初始在0
  • 这样删倒数n,快慢就相隔n-1,即快指针移动n-1
    • 当快.next为空,则慢.next就是要删除的节点
 /**
倒数2  删4
    1   2   3   4   5
s   f
s       f    
    s       f
        s       f
            s       f
快指针初始在1,慢指针初始在0
这样删倒数n,快慢就相隔n-1,即为快指针先移动n-1
当快.next为空,则慢.next就是要删除的节点
*/

 //双指针
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {

        //初始哑节点作指向链表
        //这样才能让后面操作是基于dummy链表修改
        ListNode dummy = new ListNode();
        dummy.next = head;


        //初始快慢指针
        ListNode first = new ListNode();
        first = dummy.next;
        ListNode second = new ListNode();
        second = dummy;

        for (int i = 0; i < n-1; i++) {
            first = first.next;
        }

        while (first.next != null) {
            first = first.next;
            second = second.next;
        }

        second.next = second.next.next;

        return dummy.next;

    }
}

交换列表节点

⭐【递归思路

先往下层层递,再往上层层归,专注本级调用单元的操作

⭐主要三个要点:

  1. 返回值
  2. 调用单元做了什么操作
  3. 终止条件

本题中:

声明head【A】和next【B】

递归交换这两个相邻节点

故:

  • 返回值——交换后的子链表
    • 交换后,子链表新的头节点是NEXT【B】
  • 调用单元做了什么操作——交换链表两个节点
    • next【B】指向head【A】,B成为新头节点
    • head【A】指向后面【递归交换好】的子链表
  • 终止条件——当无节点,或者只有一个节点时截止
    • head为空或next为空
/**
【递归思路】
先往下层层递,再往上层层归

专注本级调用单元的操作
 主要三个要点:
 1、返回值
 2、调用单元做了什么操作
 3、终止条件


本题中:
声明head【A】和next【B】
递归交换这两个相邻节点
故:
1、交换后的子链表
2、交换链表两个节点
    next【B】指向head【A】,B成为新头节点
    head【A】指向后面【递归交换好】的子链表
3、当无节点,或者只有一个节点时截至
    head为空或next为空
*/
class Solution {
    public ListNode swapPairs(ListNode head) {

        //终止条件:链表只剩空节点,或一个节点时,终止
        if (head == null|| head.next == null) {
            return head;
        }
        //声明B节点
        ListNode NEXT = head.next;
        //声明后面递归部分链表
        ListNode rest = NEXT.next;

        //B指向A
        NEXT.next = head;
        //A指向后面递归好部分
        head.next = swapPairs(rest);

        //返回新的头节点
        return NEXT;

    }
}

链表相交

  • 链表相交说明两链表从这开始后面节点是相同
    • 【节点的地址相同。或者说。前一个节点的next指针,指向同一个节点地址】,不单单是节点的value相同。
    • 如A【1234】和B【1234】的节点值都相同。但A的2.next=B的2.next。则3才是第一个相交节点。2是不同地址的节点,只是value等于2
  • 节点不等时,遍历链表直到第一个节点相同

思路:

  1. 遍历A,B链表,当各自遍历到结尾时,分别拼接上遍历B、A链表。
  2. 这样最终遍历的长度一样了
  3. 如果有交点,必然在拼接部分汇合。及A = B
  4. 如果没有交点。到拼接部分的最后一个节点,A != B,都后移一位。则A、B都为null此时A、B就相等了,跳出循环。
 //链表相交说明节点相同,不是节点的value相同
 //节点不等时,遍历链表直到第一个节点相同————while
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        
        ListNode A = headA;
        ListNode B = headB;

        while (A != B) {

            A = (A == null ? headB : A.next);
            B = (B == null ? headA : B.next);
        }

        //跳出循环说明A = B 了
        return A;
    }
}

为什么while循环中不用三目运算符超时??

        while (A != B) {
            if (A == null) {
                A = headA;
            }else {
                A = A.next;
            }
            if (B == null) {
                B = headA;
            }else {
                B = B.next;
            }
        }

TIPS

ListNode cur = head;
和
ListNode cur = new ListNode(); 
cur.next = head;
  • 声明上都可以,只是一个直接是head,一个指向head

链表结束条件 cur != nullcur.next != null 区分不同情况

  1. cur != null:通常用于遍历整个链表最后一个节点也参与循环
  2. cur.next != null:通常用于在链表上执行某些操作,例如插入、删除或反转节点。在 cur 的下一个节点为空之前,您可以安全地访问 cur.next最后一个节点不参与循环

环形链表

判断是否有环形链表。有环返回成环的节点,无环返回null

  • 声明:Set<ListNode> hashSet = new HashSet<ListNode>();
  • 存:hashSet.add(节点);
  • 判断是否有重复:hashSet.contains(节点);

重点:⭐哈希Set的使用

  • HashSet 是一种基于哈希表的集合数据结构,它不是一个有序的集合。它的主要用于高效地存储和查找元素,而不是按照特定顺序访问它们。
if (hashSet.contains(nodeToFind)) { ————————判断是否存在
    // 如果节点存在于 HashSet 中 
    // 遍历 HashSet 来找到匹配的节点 
    for (ListNode node : hashSet) { ————————遍历集合
        if (node.equals(nodeToFind)) { ————————找到需要的节点
        // 找到匹配的节点 
        // 在这里使用 node 进行操作 } 
    } 
}
 /**哈希表

遍历链表【while】,把节点都存到哈希表中
判重,如果有当前节点节点,已经存在于哈希表,说明有环。
如果遍历完都不存在,返回null

存哈希表add(ListNode)
判重contains(ListNode),有为true,没有为false

*/
public class Solution {
    public ListNode detectCycle(ListNode head) {
        
        ListNode cur = head;
        Set<ListNode> hashSet = new HashSet<ListNode>();

        while (cur != null) {

            //先判断哈希有没有,再存入。
            //第一个节点开始,哈希表为空,这样写也没影响

            if (hashSet.contains(cur)) {

            //为true说明已存在,返回环的节点
                return cur;

            }else {

                //说明不存在,存入哈希
                hashSet.add(cur);

            }
            cur = cur.next;

        }
        return null;
    }
}

二叉树

  • 栈是递归的一种实现结构

    • 调用栈保证的递归能正确的层层返回
    • 上层递归先进调用栈,最后出调用栈
    • 底层后进调用栈,但是最先出,最先往上层递归返回
  • 深度优先遍历(前中后序遍历)——递归

  • 广度优先遍历(层序遍历)——队列

    • 层序是从上往下,一层层遍历。
    • 所以用队列存储可以顺序打印

深度优先遍历:前中后序遍历——递归

前序

⭐终止条件一般放在递归方法第一行

  • 入参————根节点,遍历的结果集(数组列表)。因为要输出遍历结果,传入容器存储数据。
  • 返回值————遍历方法除了把遍历数据放入结果集无其他数据操作,无返回值
  • 终止条件————当前节点为空说明本层结束
  • 每层递归操作————先add根节点进容器,再对左子节点调用遍历方法,再对右遍历。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        
        List<Integer> res = new ArrayList<Integer>();   //声明数组列表存储结果集
        
        preorder(root, res);                            //调用遍历(的递归)方法,传入根节点+结果集列表(空的)

        return res;                                     //前序遍历方法的返回值,返回遍历结果集
    }
                                                        //定义前序遍历的(递归)方法
                                                        //无返回值,是遍历方法,把结果add进入参给的结果集中
    public void preorder(TreeNode root, List<Integer> list) {                            

        if (root == null ) {                            //结束条件:方法没有返回值,如果节点为空返回上一层递归。
            return;                 
        }

        list.add(root.val);                             //前序,先把根节点add到结果集。再遍历左节点,再遍历右节点
        preorder(root.left, list);                      //调用遍历(的递归)方法,(前序)遍历左节点
        preorder(root.right, list);                     //同上调用递归遍历右节点

    }
}

⭐动态规划

  1. 确定dp数组的含义 dp[]或dp[][]
  2. 如何初始化dp[0][0]。其他数组的值有时候也需要初始化,全0、-1、100等都可以。
  3. 确定遍历顺序+递推公式

所有情况都遍历到,且递推逻辑正确。最终结果就是dp[m][n]

爬楼梯

class Solution {
    public int climbStairs(int n) {

        int[] dp = new int[n + 1];
        dp[0] = 1;
        dp[1] = 1;
        //dp[2] = 2;
        for(int i = 2; i <= n; i++){
            dp[i] = dp[i-1] + dp[i - 2];
        }
        return dp[n];
    }
}
class Solution {
    public int climbStairs(int n) {

        int[] dp = new int[n + 2];
        //dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i <= n; i++){
            dp[i] = dp[i-1] + dp[i - 2];
        }
        return dp[n];
    }
}

最小路径和

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;        //列长度
        int n = grid[0].length;     //(第0)行长度

        int[][] dp = new int[m+1][n+1];     //声明dp及含义,到(i,j)位置的最小和

        dp[0][0] = grid[0][0];      //初始化

        //确定遍历顺序+确定递推公式
        //1、第一列只能往下,第一行只能往右。对应元素只能分别由上方、左方元素递推
        //2、非第一行第一列元素,可以由上往下或左往右得到

        //遍历第一列:上往下
        for (int i = 1; i < m; i++) {               //dp[0][0]已经定义了,所以从第二个元素【下标为1】开始。
            dp[i][0] = dp[i-1][0] + grid[i][0];     //并且0 - 1,明显越界
        }
        //遍历:第一行左往右
        for (int i = 1; i < n; i++) {
            dp[0][i] = dp[0][i-1] + grid[0][i];
        }
        //遍历其他所有:先下再右
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }
}