D01代码随想录算法训练-704二分查找-27移除元素-977有序数组的平方

90 阅读9分钟

1.前置知识与隐性知识

前置知识

数组是存放在连续内存空间上的相同类型数据的集合

因为要保证内存地址的连续性,删除和增添元素的时候会移动其他元素的地址,为了保持数组的不变性,很多语言并没有提供直接删除数组元素的方式

数组下标为什么从零开始 访问元素时需要知道元素对应的内存地址,数组指向的内存的地址为首元素的地址 即 array[0] 所以第n个元素是 array[n] = array[0] + size * n 如果是从1开始计算,那么第n和元素是 array[n] = array[0] + size * (n - 1) 从1开始编号比从0开始编号每次获取内存地址都多了一次 减法运算,也就多了一次cpu指令的运行。这也是数组从0下标开始访问一个原因。

优点

可以通过下标更快的查找元素,遍历数组的时候非常快,cpu会有缓存,cpu读取缓存只能读取连续内存的内容,数组的连续性符合cpu缓存的指令原理

缺点

  1. 为了保证连续性,增删会产生大量的元素移动工作
  2. 要求内存连续,如果不存在的话在一些语言中会触发垃圾回收
  3. 数组扩容很多都是通过一个容量更大的数组来存放,只是对于使用者来说是透明的
  4. 数组可能会出现越界,导致内存地址异常

数组板块涉及到的算法思想

  • 双指针
  • 滑动窗口(双指针扩展)
  • 区间划分
  • 前缀和

隐性知识

(一)底层机制认知

  1. 内存连续性带来的缓存优化

    • 行优先遍历效率高于列优先(空间局部性原理) 
    • 预分配空间策略减少内存碎片(动态数组倍增扩容) 
  2. 双指针法的变体应用

    • 快慢指针:标记有效区间(移除元素) 
    • 首尾指针:处理有序特征(有序数组平方) 
    • 滑动窗口:动态调整区间(最小子数组) 
  3. 边界条件处理模式

    • 循环不变量原则(二分查找区间定义) 
    • 整数溢出预防(mid=left+(right-left)/2)

(二)高阶技巧

  1. 前缀和与哈希表结合

    • 快速计算子数组和(和为K的子数组) 
    • 二维前缀和预处理(矩阵区域极值问题) 
  2. 时空权衡策略

    • 暴力解法→双指针优化(时间复杂度从O(n²)→O(n)) 
    • 空间换时间(多数元素问题使用哈希表计数)
  3. 问题转换技巧

    • 求和转求差(两数之和问题) 
    • 极值问题转区间收敛(长度最小子数组)

(三)思维模式

  1. 模式迁移能力

    • 数组双指针→链表操作(快慢指针找中点) 
    • 螺旋矩阵填充→图像处理路径规划
  2. 测试用例设计

    • 全0数组测试边界(多数元素问题)
    • 连续重复值验证算法鲁棒性(移除元素)

2.算法详解

704二分查找

解决二分查找有3种解法,左闭右开/左闭右闭/递归(待补充)

我的初次尝试解法

import java.util.ArrayList;
import java.util.List;
public class leetcode704 {
    /**
     * 没有一把过的原因 nums[mid] < target  写错成了 nums[mid] > target
     * @param nums
     * @param target
     * @return
     */
    public static int search(int[] nums, int target) {

        int left = 0;
        int right = nums.length - 1;
        int mid = (left + right) / 2;

        if(nums[left] > target || nums[right] < target){
            return -1;
        }

        while ( left<=right) {
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > target) {
                //遍历左侧
                right = mid - 1;
                mid = (left + right) / 2;
            } else if (nums[mid] < target) {
                //遍历右侧
                left = mid + 1;
                mid = (left + right) / 2;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        int[] nums =  new int[]{-1,0,3,5,9,12};
        List<Integer> integers = new ArrayList<>();
        integers.add(-1);
        integers.add(0);
        integers.add(3);
        integers.add(5);
        integers.add(9);
        integers.add(12);
        int search = search(nums, 3);
        System.out.println(search);
    }
}

27移除元素

两种解法:暴力破解(待补充)/双指针法

双指针解法

public class leetcode27 {
    public static 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;
    }

    public static void main(String[] args) {
        int[] nums = new int[]{3,2,2,3};
        int[] nums2 = new int[]{0,1,2,2,3,0,4,2};
        int val = 2;
        int i = removeElement(nums2, val);
        System.out.println(i);
        for (int i1 = 0; i1 < nums2.length; i1++) {
            System.out.print(nums2[i1]+" ");
        }

    }
}

977有序数组的平方

题目描述

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 请你设计时间复杂度为 O(n) 的算法解决本问题

示例 1:

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

直接排序法(比较偷懒)

public int[] sortedSquares(int[] nums) {
    int[] ans = new int[nums.length];
    for (int i = 0; i < nums.length; ++i) {
        ans[i] = nums[i] * nums[i];
    }
    Arrays.sort(ans);
    return ans;
}

双指针法

public class leetcode977 {

    public static int[] sortedSquares(int[] nums) {
        //用Math求平方 3的值

        int[] nums_out = new int[nums.length];

        int right = nums.length - 1;
        int left = 0;
        //用Math求平方 left的值

        int i = left;

        while (i<nums.length){
            if (getPowInt(nums[left])> getPowInt(nums[right])) {
                nums_out[nums.length - 1 - i] = getPowInt(nums[left]);
                left++;
            } else {
                nums_out[nums.length - 1 - i] = getPowInt(nums[right]);
                right--;
            }
            i++;

        }
        return nums_out;
    }

    public  static  int getPowInt(int value){
        return (int)Math.pow(value,2);
    }

    public static void main(String[] args) {
        int[] nums = new int[]{-4, -1, 0, 3, 10};
        int[] ints = sortedSquares(nums);
        for (int i = 0; i < ints.length; i++) {
            System.out.println(ints[i]);
        }
    }
}

3.算法听想变用

听(反学习)

专家视角:看到内存布局与缓存机制的关系

新手误区:仅关注语法不思考物理存储

想(参考答案思维)

  1. 写代码的时候需要思考所有的已知条件是否使用到了,也就是最开始就有梳理好已知条件的个数,然后解题的时候对比自己使用到了哪里,如果没用到的也能解出,要么是方法不够优秀或者解法不对
  2. 数组一般是大小提前确定的,也就是左右边界固定,那么操作的时候就容易联想从后往前,左右开工的双指针
  3. 数组可以对应数学里的区间概念,因此解题时把数据结构翻译成对应的数学概念能更快找到解题线索(计算机编程与数学概念映射关系的梳理)

解题核心原则(AI生成)

  1. 条件穷举与验证

    • 列出题目所有显性/隐性条件(数组特性、边界约束、数据范围等)
    • 每写一行代码前确认:当前使用的条件是否覆盖所有必要信息
    • 解法完成后验证:未使用的条件可能是优化提示或错误标志
      例:旋转数组搜索题必须同时使用有序性旋转点特性
  2. 数据结构映射

    数组物理结构 → 数学区间概念
    | 数组表示      | 数学区间  | 长度计算   | 有效索引          |
    |---------------|----------|------------|------------------|
    | `arr[0..n-1]` | [0, n-1] | n          | 0 ≤ i ≤ n-1      |
    | 左闭右闭      | [L, R]   | R-L+1      | L ≤ i ≤ R        |
    | 左闭右开      | [L, R)   | R-L        | L ≤ i < R        |
    

变(深层迁移)

模式提炼的四个层次(以数组为例)

  1. 物理结构模式

    • 案例:数组内存连续性 → 迁移到缓存行预取机制
    • 思考:当处理矩阵乘法时,如何通过分块访问(blocking)提升缓存命中率?
  2. 逻辑操作模式

    • 案例:双指针快慢指针 → 迁移到链表环检测
    • 思考:在解决"删除排序链表重复元素(82题)"时,如何调整快慢指针的移动策略?
  3. 数学抽象模式

    • 案例:数组下标映射 → 迁移到哈希函数设计
    • 思考:当处理"两数之和(1题)"时,如何将数学互补关系转化为哈希表键值对?
  4. 系统思维模式

    • 案例:滑动窗口时间复杂度控制 → 迁移到实时流数据处理
    • 思考:在解决"最长无重复子串(3题)"时,如何将该模式应用于实时日志分析场景?

深层迁移的思考框架

1. 模式解耦(Decouple)
   - 关键问题:当前模式的核心约束条件是什么?
   - 案例:二分查找的核心是"有序性+上下界",迁移到非数值场景时需保留这两个条件

2. 模式重构(Refactor)
   - 执行步骤:
     ① 参数替换:将数组索引替换为时间戳(如日程安排问题)
     ② 维度扩展:将一维指针升级为二维坐标(如岛屿问题)
     ③ 条件松弛:允许部分有序性缺失(如旋转数组搜索)

3. 模式验证(Validate)
   - 验证矩阵:
   | 原场景         | 新场景         | 模式匹配度 | 需调整点          |
   |----------------|----------------|------------|-------------------|
   | 数组去重       | 流数据去重     | 70%        | 增加LRU淘汰机制   |
   | 双指针求和     | 三数之和       | 85%        | 增加第三指针约束  |

用(聚焦+模式化)

聚焦-->不变&经典

不管数组的算法如何改变条件,关于数组数据结构的性质是不变的,例如大小固定不能删除只能覆盖

系统化训练建议:

  1. 每道题用表格列出所有条件及其使用位置
  2. 手动模拟2个元素/3个元素的边界案例
  3. 比较不同区间写法的转换关系([L,R] ⇄ [L,R))
  4. 总结个人易错点形成自查清单

模式化-->代码模板

二分查找模板

// 1. 左闭右闭 [L,R]
    int L = 0, R = arr.length - 1;
    while (L <= R) {
        int mid = L + (R - L) / 2;
        if (arr[mid] == target) return mid;      // 精确命中
        else if (arr[mid] < target) L = mid + 1; // 砍左
        else R = mid - 1;                        // 砍右
    }
    return -1;                                   // 未找到


// 2. 左闭右开 [L,R)
    int L = 0, R = arr.length;
    while (L < R) {
        int mid = L + (R - L) / 2;
        if (arr[mid] >= target) R = mid;         // 右开:保留 mid 但不包含
        else L = mid + 1;                        // 左闭:排除 mid
    }
    return L;    

遇到有序数组问题时自动触发

if (problem.contains("sorted array") || input.isSorted()) {
    // 第一反应选项:
    1. 二分查找(时间复杂度O(logn))
    2. 双指针逼近(时间复杂度O(n))
    3. 哈希表辅助(空间换时间O(n))
    // 决策树:
    if (需要找特定值) → 选项1
    else if (需要找组合) → 选项2
    else → 选项3
}

额外补充

晚上刷到吴军老师的计算之魂的介绍,其中有3个计算机的核心思想 递归/递推分治(归并排序),平衡(tradeoff|安全/性能/成本)例如CPU多级缓存存储的平衡

数组算法解题钥匙串

暴力枚举 → 哈希表优化 → 双指针法 → 滑动窗口

↑↓

时间复杂度:O(n²)→O(n)

←→

空间复杂度:O(1)→O(n)

参考链接

notes.kamacoder.com/questions/5…

AI对话梳理思路 chat.deepseek.com/a/chat/s/66… monica.cn/home/chat/%…