算法day01-数组篇(1)

前言

  从今天开始,打算正式记录一下我学习算法的过程。之前刷题总是零零散散的,想到就做几道题,很多思路也没系统整理过。这次想换个方式,一边刷题一边写笔记,把每类题型的解法、常见套路、易错点都总结下来,方便以后复习,也算是给自己的学习留点痕迹。刚开始会从一些基础题入手,比如二分查找、双指针、模拟题之类的,后面逐步过渡到动态规划、图论、搜索等稍微难一些的部分。不会追求速度,主要还是想把题目吃透,写出自己真正理解的代码。如果能坚持下去,希望某天回头看这些笔记,能看到自己一点一点积累下来的成长。😊

开始

(一)二分查找

  这里我们选择力扣704题来训练(https://leetcode.cn/problems/binary-search/),这是一道最基本的二分查找题,题目描述是:

  1. 第一种做法是考虑左闭右闭的区间 [left, right]

class Solution {
    public int search(int[] nums, int target) {
        int left = 0 , right = nums.length - 1;    //这里因为右边是闭区间
        while(left <= right){
            int mid = left + (right - left)/2;      //防止left+right溢出
            if(target == nums[mid]){
                return mid;
            }else if(target < nums[mid]){       //不能包含mid,这样会重叠
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        return -1;      //找不到
    }
}

//时间复杂度:O(log n)
//空间复杂度:O(1)

  2. 第二种做法是考虑左闭右开的区间 [left, right)

class Solution {
    public int search(int[] nums, int target) {
        int left = 0 , right = nums.length;         //这里right是不被包含在区间里的
        while(left < right){
            int mid = left + (right - left)/2;      //防止溢出
            if(target == nums[mid]){
                return mid;
            }else if(target < nums[mid]){       
                right = mid;            //因为下一个区间不会包含mid
            }else{
                left = mid + 1;
            }
        }
        return -1;      //找不到
    }
}

【相关题目】

  • 力扣35题搜索插入位置【简单】:https://leetcode.cn/problems/search-insert-position/
  • 力扣34题在排序数组中查找元素的第一个和最后一个位置【腾讯面试手撕,中等题】:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/

(二)移除元素

  这里我们选择力扣27题(https://leetcode.cn/problems/remove-element/description/)来练习,题目描述是:

  

 

   1. 暴力方法(锻炼代码实现能力)  

  主要思路是:先将数组排序,然后通过两次二分查找的方法找到val的起始和终止位置,然后将终止位置后面的元素覆盖到start-end这部分来,若后面的元素少于start-end这片区域,多余的没覆盖的元素可以忽略,不用处理。

  【注意】若题目要求不能改变原数组元素的相对顺序,这样做就不对了!

class Solution {
    public int removeElement(int[] nums, int val) {
        if(nums == null || nums.length == 0){       //若该数组为空
            return 0;
        }
        Arrays.sort(nums);
        int left = 0, right = nums.length - 1;
        int start = -1, end = -1;
        while(left <= right){
            int mid = left + (right - left)/2;
            if(val == nums[mid]){
                start = mid;        //暂时确定为mid
                right = mid - 1;        //收缩左边界,继续找第一个出现的元素
            }else if(val < nums[mid]){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        if(start == -1){
            return 0;
        }

        left = 0;
        right = nums.length - 1;
        while(left <= right){
            int mid = left + (right - left)/2;
            if(val == nums[mid]){
                end = mid;        //暂时确定为mid
                left = mid + 1;        //收缩左边界,继续找最后一个出现的元素
            }else if(val < nums[mid]){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }

        int j = start;
        //此时找到了start和end
        for(int i = end + 1; i < nums.length; i++){
            nums[j] = nums[i];      //后面的元素依次覆盖前面的
            j++;
        }

        return nums.length - (end - start + 1);

    }
}

//时间复杂度:O(n log n)
//空间复杂度:O(1)

  2. 双指针(这里是快慢指针,精髓!!)

  主要思路是:设置一个left指针和right指针,right指针负责”探路“(看当前值是否等于val),left就是慢指针用于存放不等于val的数,这样最后返回的数组就把目标元素都放后面了。

class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0;
        for(int right = 0 ; right < nums.length; right++){
            if(nums[right] != val){
                nums[left] = nums[right];
                left++;
            }
        }
        return left;
    }
}
//时间复杂度:O(N)
//空间复杂度:O(1)

【扩展】Arrays.sort()的实现

  java7以前,采用的是Dual-Pivot Quicksort传统快排,java7以后采用的是双轴快排。

import java.util.Arrays;

public class QuickSortDemo {
    public static void quickSort(int[] arr, int left, int right){
        if(left<=right) return;
        //选取基准值
        int pivot = arr[left];
        int i = left;
        int j = right;
        while(i < j){
            while(i<j && arr[j] >= pivot)   j--;        //如果没碰到比基准值小的,则右指针一直收缩
            while(i<j && arr[i] <= pivot)   i++;
            if(i<j){
                swap(arr, i, j);
            }
        }

        //把pivot放到中间位置
        swap(arr, left, i);

        //递归处理左右两边
        quickSort(arr, left, i-1);
        quickSort(arr, i+1, right);
    }

    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void main(String[] args){
        int[] nums = {5,2,9,1,6,3,8};
        quickSort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums));
    }
}
//时间复杂度:O(nlogn),最差情况下是O(n^2)
//空间复杂度:O(logn)

 

【快慢指针相关题目】

  • 283题移动零的位置:https://leetcode.cn/problems/move-zeroes/description/?envType=problem-list-v2&envId=2cktkvj
class Solution {
    public void moveZeroes(int[] nums) {
        int left = 0;
        for(int right = 0; right < nums.length; right++){
            if(nums[right] != 0){
                int tmp = nums[left];
                nums[left] = nums[right];
                nums[right] = tmp;
                left++;
            }
        }
    }
}
//时间复杂度:0(N)
//空间复杂度:O(1)
  •  后面还会有链表相关的快慢指针的题目

(三)有序数组的平方

   这道题主要也是了解双指针的做法,比较巧妙。

  主要思想:

  1. 暴力解法:从第一个数平方到最后一个数,最后把数组排序。

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

  2. 双指针法(这里主要是首尾指针)

【注意:这里新建的数组把平方数存放进去要从末尾开始放!!因为可能存在负数,负数的平方可能比某些正数的平方要大】

class Solution {
    public int[] sortedSquares(int[] nums) {
        int[] ans = new int[nums.length];
        int left = 0, right = nums.length - 1;
        for(int i=nums.length-1; i>=0; i--){
            int x = nums[left] * nums[left];
            int y = nums[right] * nums[right];
            if( x >= y ){           //把大的数放后面
                ans[i] = x;
                left++;
            }else{
                ans[i] = y;
                right--;
            }
        }
        return ans;
    }
}
//时间复杂度:O(N)
//空间复杂度:O(1)

 

【首尾指针相关题目】

  • 11题盛最多水的容器:误区(并不是取两个高度最高的围起来,因为宽度可能不够大)

  

import java.util.*;
class Solution {
    public int maxArea(int[] height) {
        //长度短的那一边才要移动,因为整体的面积受短的边控制
        int left = 0, right = height.length-1;
        int maxArea = 0;
        while(left < right){            //等于的时候围不成
            int h = Math.min(height[left], height[right]);
            int width = right - left;
            int area = h * width;
            maxArea = Math.max(maxArea, area);
            if(height[left] < height[right]){
                left++;
            }else{
                right--;
            }
            
        }
        return maxArea;

    }
}
//时间复杂度:O(N)
//空间复杂度:O(1)
  • 15三数之和:这里需要考虑去重的情况。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        //主要思路:首先遍历整个数组,确定一个基准的数,然后后面的数用首尾指针来查找
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList<>();
        for(int i=0; i<nums.length; i++){
            //如果后一个与前一个数相同,则跳过
            if(i>0 && nums[i] == nums[i-1]){
                continue;
            }

            int target = -nums[i];
            int left = i+1, right = nums.length-1;
            while(left < right){
                int sum = nums[left] + nums[right];
                if(target == sum){
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    //这里需要考虑去重
                    while(left < right && nums[left] == nums[left+1]){
                        left++;
                    }
                    while(left < right && nums[right] == nums[right-1]){
                        right--;
                    }
                    left++;
                    right--;
                }else if(target < sum){
                    right--;
                }else{
                    left++;
                }
            }
        }
        return res;
    }
}
//时间复杂度:O(n^2),排序O(nlogn)+双指针
O(n^2),以后者为主
//空间复杂度:O(1),res是结果本身,除了排序就是就地排序,不需要额外空间
  • 42接雨水:https://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&envId=top-100-liked

  

class Solution {
    public int trap(int[] height) {
        //这道题的主要思路是:每个点能接的雨水量与该点前后缀的最大值有关
        int n = height.length;
        int[] preMax = new int[n];
        preMax[0] = height[0];      //初始化
        for(int i=1; i<n; i++){
            preMax[i] = Math.max(preMax[i-1], height[i]);
        }
        int[] sufMax = new int[n];
        sufMax[n-1] = height[n-1];
        for(int i=n-2; i>=0 ;i--){
            sufMax[i] = Math.max(sufMax[i+1], height[i]);
        }

        int ans = 0;
        for(int i=0; i<n; i++){
            //累加每个点积累的雨水
            ans += Math.min(sufMax[i], preMax[i]) - height[i];
        }
        return ans;
    }
}
//时间复杂度:O(N)
//空间复杂度:O(N)

 

总结

   这篇笔记算是我算法学习的一个小起点,主要记录了二分查找、移除元素、和有序数组平方这三个常见题型。练习过程中,重新理解了二分查找的区间写法区别([left, right] 和 [left, right)),还有一些容易忽略的细节,比如防止溢出这种小技巧。

  移除元素那题对比了暴力解法和双指针,发现双指针真的挺巧妙,不光写法简单,效率也高。而有序数组平方这题,一开始会觉得暴力平方+排序也能做,但双指针从两端比较然后倒着放结果数组,真的思路很清奇也很好用。

  整体来说,写代码的时候顺便理清了思路,也发现自己平时有些写法理解得还不够细。这只是个开始,后面我会继续把常见题型都过一遍,慢慢把算法打扎实。💪

 

参考:代码随想录https://programmercarl.com/

posted @ 2025-04-23 23:54  筱倩  阅读(534)  评论(0)    收藏  举报