学习刷题-1

2.29

时间复杂度

  • 时间、空间复杂度优先关注

  • 只要时间复杂度达到 O(n3)及以上,工程上就没有意义了

  • n是‘数据量级

    • 这里面的 2的x方为n,所以x取log(n)

数组

代码随想录 (programmercarl.com)

  • C++中可以看到数组的每个元素地址连续,例如int类型的两元素地址相差4;地址16进制

  • Java的地址,Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。;地址16进制

二分查找

题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件。区间的定义就是不变量

  • 升序数组,不重复 -- 所以查middle和target的关系

  • 锁定了方向后,重新比较新的区间里middle和target的关系

  • 两种区间方法:左闭右闭;左闭右开

 class Solution {
     public int search(int[] nums, int target) {
         int left = 0;
         int right = nums.length - 1;
 ​
         while (left <= right) {
             int middle = left + (right - left) / 2;
             if (nums[middle] > target) {
                 right = middle - 1;
             } else if (nums[middle] < target) {
                 left = middle + 1;
             } else {
                 return middle;
             }
         }
         return -1;
     }
 }
时间复杂度 O(log n)

以每次将搜索范围缩小一半的速度来看,当搜索范围缩小到 1 时就可以确定是否存在目标值,或者搜索范围为空。假设初始搜索范围的长度为 n,那么经过 k 次迭代后,搜索范围的长度将变为 n / 2^k。当搜索范围的长度为 1 时,即 n / 2^k = 1,解得 k = log₂(n)。

移除元素

暴力法

双指针法

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组

  • 慢指针:指向更新 新数组下标的位置

 class Solution {
     public int removeElement(int[] nums, int val) {
         int slowIndex = 0;
         for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
             if (nums[fastIndex] != val) {
                 nums[slowIndex] = nums[fastIndex];
                 slowIndex++;
             }
         }
         return slowIndex;
     }
 }
  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

3.1

有序数组的平方
双指针法

数组其实是有序的, 只不过负数平方之后可能成为最大数了。

那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。

此时可以考虑双指针法了,i指向起始位置,j指向终止位置。

定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。

如果A[i] * A[i] < A[j] * A[j] 那么result[k--] = A[j] * A[j];

如果A[i] * A[i] >= A[j] * A[j] 那么result[k--] = A[i] * A[i];

新建固定长度的数组:

 // 创建一个固定长度为10的整型数组
 int[] array = new int[10];
 class Solution {
     public int[] sortedSquares(int[] nums) {
         int left = 0;
         int right = nums.length - 1;
         int[] result = new int[nums.length];
         int index = result.length - 1; 
         
         while (left <= right) {
             int leftSquare = nums[left] * nums[left];
             int rightSquare = nums[right] * nums[right];
 ​
             if (leftSquare >= rightSquare) {
                 result[index--] = leftSquare;
                 left++;
             } else {
                 result[index--] = rightSquare;
                 right--;
             }
         }
         return result;
     }
 }
  • 旧数组左右双指针,判断哪个大

  • 新数组的新指针,递减从右开始排列放数值大的数

  • 时间复杂度为 O(n),其中 n 是数组 nums 的长度。这是因为我们只需一次遍历数组就可以完成平方、比较和填充结果数组的操作。

    空间复杂度也是 O(n),因为我们创建了一个与输入数组相同长度的结果数组,用于存储平方后的值。

长度最小的子数组
滑动窗口

接下来就开始介绍数组操作中另一个重要的方法:滑动窗口

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。

那么滑动窗口如何用一个for循环来完成这个操作呢。

首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。

如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?

此时难免再次陷入 暴力解法的怪圈。

所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。

那么问题来了, 滑动窗口的起始位置如何移动呢?

这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

         int result = Integer.MAX_VALUE;

Integer.MAX_VALUE 是 Java 中 int 类型的最大值,它的值为 2147483647。在计算机中,int 类型通常占用 32 位,其中一位用于表示符号位,所以 int 类型的最大值是 2^31 - 1。这个值在进行整数运算时通常用于初始化一个变量,表示这个变量的初始值为整型的最大值。

         return result == Integer.MAX_VALUE ? 0 : result;

这行代码的意思是,如果 result 的值等于 Integer.MAX_VALUE,则返回 0,否则返回 result 的值。这样的逻辑通常用于处理特殊情况,例如在某些算法中,当无法找到符合条件的结果时,返回一个特殊的值(这里是 0),以表示没有找到结果。

 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++];
             }
         }
         return result == Integer.MAX_VALUE?0:result;
     }
 }
  • 遍历窗口右端,累加求和得sum

  • 如果sum符合条件,则保留最小的 窗口长度,并从窗口左端减少sum继续观察while

  • 时间复杂度

    • 循环:代码中有一个外层的for循环,遍历数组nums一次。对于每个右边界right,内部while循环可能会执行多次,但每次执行都会导致左边界left向右移动。注意,左边界left和右边界right在整个过程中各自只会从左向右遍历数组nums一次。

    • 总体:尽管存在嵌套循环,但每个元素最多被访问两次(一次被添加到sum中,一次从sum中减去)。因此,总的时间复杂度为O(n),其中n是数组nums的长度。

    空间复杂度

    • 额外空间:代码中使用的额外空间包括几个基本类型的变量:leftrightsumresult。它们的空间占用与输入数组的长度无关。

    • 总体:因此,空间复杂度为O(1),即常数空间复杂度。

    总结

    • 时间复杂度:O(n)

    • 空间复杂度:O(1)

螺旋矩阵II
 class Solution {
     public int[][] generateMatrix(int n) {
         int matrix[][] = new int[n][n];
         int loop = 0; // 控制循环圈数
         int i,j;
         int start = 0;
         int count = 1;
         while(loop++ < n/2){ // 后面再考虑奇数的情况,第一次这里loop已经等于1了
             for(j=start;j<n-loop;j++){
                 i = start;
                 matrix[i][j] = count++; // 注意这里初始i要是start不然没有赋值
             }
             //此时j = n-loop-1
             for(i=start;i<n-loop;i++){
                 matrix[i][j] = count++;
             }
             //此时i = n-loop-1
             for(;j>=loop;j--){
                 matrix[i][j] = count++;
             }
             //此时j = 0
             for(;i>=loop;i--){ // 注意这里i>loop
                 matrix[i][j] = count++;
             }
             start++;
         }
         if(n % 2 == 1){
             matrix[start][start] = count;
         }
         return matrix;
     }
 }
  • 要设置循环(圈数);循环还要考虑奇数的情况

  • 一圈顺时针保持 【左开右闭】

  • 开始的位置要设置start;这个start每个循环还要累加

  • for循环(i;i<n;i++)这个格式不能变:for (初始化语句; 循环条件检查; 步进语句);也可以int i = 0; for (; i < n; i++)

    • loop++ < n/2:这个条件确保了算法只处理到矩阵的一半深度,因为螺旋填充是对称的。当n是奇数时,中心点会单独处理。

    • start++:每完成一层的填充后,start递增,意味着下一轮填充的起始点向内移动一格。

  • 算法复杂度

    • 时间复杂度:O(n^2),因为需要填充整个n×n的矩阵,每个元素只访问一次。

    • 空间复杂度:O(1),不考虑输出矩阵所占用的空间。算法本身只使用了固定数量的变量(loopijstartcount)进行计算。



链表

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。

  • 单链表

  • 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。

  • 循环链表:首尾相连。循环链表可以用来解决约瑟夫环问题。


链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。


链表的定义
 public class ListNode {
     int value;
     ListNode next;
 ​
     ListNode(int value) {
         this.value = value;
         this.next = null;
     }
 ​
     public static void main(String[] args) {
         // 创建链表节点
         ListNode head = new ListNode(1);
         head.next = new ListNode(2);
         head.next.next = new ListNode(3);
 ​
         // 打印链表
         ListNode temp = head;
         while (temp != null) {
             System.out.println(temp.value);
             temp = temp.next;
         }
     }
 }
 class ListNode:
     def __init__(self, value=0, next=None):
         self.value = value
         self.next = next
 ​
 # 创建链表
 head = ListNode(1)
 head.next = ListNode(2)
 head.next.next = ListNode(3)
 ​
 # 打印链表
 temp = head
 while temp:
     print(temp.value)
     temp = temp.next

  • 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

  • 链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

移除链表元素
         ListNode dummy = new  ListNode(-1,head)

通过创建一个值为 -1 的虚拟节点,并将其下一个节点指向 head,我们可以在链表的头部插入新的节点时,统一操作,无论是否有头部节点存在。这样可以避免在插入操作时需要处理头部节点为空的特殊情况。

让我们通过一个简化的图解来说明这个过程。假设我们有一个链表,我们要移除所有值为 val 的节点。这里我们用 val = 2 作为例子。

初始链表(要移除所有值为2的节点):

 rustCopy code
 1 -> 2 -> 3 -> 2 -> 4

我们引入一个虚拟头节点(dummy head)以简化边界条件,这样我们就不需要单独处理头节点可能被移除的情况。

步骤 0: 引入虚拟头节点

 rustCopy code
 dummy -> 1 -> 2 -> 3 -> 2 -> 4

现在,pre 指向 dummy 节点,cur 指向第一个实际节点(值为1)。

步骤 1: cur 指向1, 它不等于2,所以我们不移除它。pre 现在需要指向 cur,因为 cur 是新的“前一个节点”。

 rustCopy codepre
 v
 dummy -> 1 -> 2 -> 3 -> 2 -> 4
        ^
        cur

步骤 2: 移动 cur 到下一个节点(值为2),这个节点需要被移除。我们将 pre.next 设置为 cur.next,跳过了值为2的节点。

 rustCopy codepre
 v
 dummy -> 1    2 -> 3 -> 2 -> 4
        \        ^
          ------->   
                   cur

注意,在这一步,我们没有移动 pre,因为我们移除了 cur 指向的节点,pre 应该保持指向上一个有效节点。

步骤 3: 继续移动 cur 到下一个节点(值为3),它不等于2,所以我们保留它。此时,我们需要更新 pre 指向新的“前一个节点” cur

 rustCopy code          pre
         v
 dummy -> 1 -> 3 -> 2 -> 4
              ^
              cur

步骤 4: cur 移动到下一个节点(值为2),这个节点需要被移除。我们再次将 pre.next 设置为 cur.next,跳过了值为2的节点。

 rustCopy code          pre
         v
 dummy -> 1 -> 3    2 -> 4
              \      ^
                ----->  
                     cur

步骤 5: 最后,cur 移动到值为4的节点,它不等于2,我们保留它。此时链表中所有值为2的节点都已被移除。

最终链表:

 rustCopy code
 dummy -> 1 -> 3 -> 4

通过这个图解,你可以看到 pre 仅当 cur 指向的节点被保留(即不等于 val)时才向前移动。这样,pre 始终指向最后一个未被移除的节点,确保我们可以正确地跳过(即移除)所有值等于 val 的节点。希望这个图解能帮助你更好地理解算法的逻辑。

 /**
  * 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 removeElements(ListNode head, int val) {
         if (head == null){
             return head;
         }
         ListNode dummy = new  ListNode(-1,head);
         ListNode pre = dummy;
         ListNode cur = head;
         while(cur != null){
             if(cur.val == val){
                 pre.next = cur.next; 
                 // 如果当前cur等于val,那么跳过该节点,所以pre.next应该跳过等于cur.next。构建连接
             }else{
                 pre = cur;
                 // 逻辑上的移动:当我们说 pre = cur 时,我们的意图是将 pre 指针逻辑上移动到 cur 的位置,而不是改变任何节点之间的物理链接。这样做是为了在下一次迭代中,如果需要从链表中移除 cur 指向的节点,我们能正确地做到这一点。
             }
 ​
             cur = cur.next;
         }
         return dummy.next;
     }
 }
  • 添加虚拟头节点: ListNode dummy = new ListNode(-1,head);注意返回第一个节点时要是虚拟节点的下一个dummy.next;。链表重在顺序

  • 如果链表是空的,没有任何节点可以被处理或移除,则返回空

  • 双指针precur,需要跳过的pre.next = cur.next; 不需要跳过的pre = cur;

  • pre.next = cur.next;

    这行代码直接修改了链表的结构。它将 pre 节点的 next 指针指向了 cur 节点的 next 节点,从而在链表中跳过了 cur 节点。这是删除操作的核心,因为它确保了 cur 节点被逻辑上从链表中移除:pre 节点现在直接连接到 cur 节点的下一个节点,而 cur 节点没有任何节点指向它(假设没有其他引用指向 cur),因此在链表的上下文中被移除了。

    pre = cur.next;

    这行代码仅仅是移动了 pre 指针,让它指向 cur 节点的下一个节点。这个操作并不修改链表的结构,也不会从链表中移除任何节点。实际上,它只是改变了 pre 指针的位置,但 cur 节点仍然保留在链表中,并且仍然被 pre 的前一个位置(即操作前的 pre 节点)指向。因此,这并不满足删除节点的目标。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值