算法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/