代码随想录算法训练营第一天|数组理论基础、704 二分查找、27 移除元素
1.数组理论基础
文章链接:https://programmercarl.com/%E6%95%B0%E7%BB%84%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
什么是数组?
数组是存放在连续地址空间中相同数据类型的集合。
数组可以直接通过下标索引找到对应的数据,数组下标一般从0开始。
由于在数组中,所有的地址空间都是连续的,那么当我需要删除或者添加一个元素的话应该怎么办呢?
对于删除元素来说:
例如我需要删除下标为3的元素的话,那么我们不能直接删除掉其所对应的空间,不然数组不再连续。所以我们做的是,需要将J这个数据删除掉,而不是删除空间。删除数据的话我们可以直接将后面的数据依次往前覆盖。
那么添加元素来说就相对简单了,只需要再数组后面开辟出需要的空间,将对应位置的数据依次后移即可添加了。
对于数组需要主要三点:
- 数组地址空间连续
- 数组下标从0开始
- 数组中的数据不能删除,只能覆盖
了解一维数组之后,那么二维数组就没那么困难了
二维数组的内存是怎么分布的呢?
对于不同的编程语言,二维数组在内存中的分布可能会有所不同。
例如在C++中,二维数组的内存空间是连续分布的,证明如下
void test_arr() {
int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}
int main() {
test_arr();
}
测试结果如下:
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
由此可知,在c++中,存放二维数组实际上就是在一串连续的地址空间上存储的。
而在Java中,二维数组的空间又是怎么分布的呢?
由于在java中是无法看到每一个元素的地址的,在这里我们就只看首行的地址,若首行地址都是有规律的,那么可以证明是连续分布的,否则就是分散的。
public static void test_arr() {
int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
}
测试结果如下:
[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05
在这可以看出来,每一行的首地址是分散开的,没有任何规律。查阅相关的资料之后,我们会发现在Java中二维数组的空间分布是像这样的。
也就是,在Java中,会有一块连续的地址空间来存放二维数组的每行的行首地址,这个行首地址是分散开的。通过行首地址就能找到对应行中的所有元素,而每一行就相当于是一个一维数组,也就是连续的空间。
2.leetcode 704 二分查找
题目链接:https://leetcode.cn/problems/binary-search/
文章链接:https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html
视频链接:https://www.bilibili.com/video/BV1fA4y1o715
题目描述:
对于本题来说,最开始想到的肯定就是暴力一次for循环解决,通过一次遍历就可以找到对应的target,时间复杂度为O(n),也是挺可观的,但是在想想过后,我们会发现数组是升序的,那么在一个升序数组中找到对应的target,我们就可以采用二分法了,时间复杂度O(log n)。
方法一:一次遍历
class Solution {
public int search(int[] nums, int target) {
int result=-1;
for(int i=0;i<nums.length;i++){
if(nums[i]==target){
result=i;
break;
}
}
return result;
}
}
方法二:二分法
二分查找的区间一般有两种写法:左闭右闭[left,right],或者左闭右开[left,right)
那么这两种的区别在哪呢?循环终止条件是什么?是left<right?还是 left<=right?,当nums[mid]>target时,应该时right=mid,还是right=mid-1
假设数组中只有一个元素[1],对于左闭右闭:那么二分区间应该是[1,1],也就是说当left== right时,也是需要进行判断的,所以循环终止条件应该为left<=right。而对于左闭右开应该是[1,2),所以不需要判断left==right时的情况,在left<right时就已经判断结束。
在我看来,这两种二分方法都是一样的,只不过是每次二分之后,得保证下次需要判断的元素全部在这个区间内,所以对于左闭右闭,当nums[mid]>target时 right=mid-1; 因为nums[mid]已经判断过,而下一个区间就是[left,mid-1]了,因为是右闭,所以会包含mid-1;而对于左闭右开,当nums[mid]>target时 right=mid;因为nums[mid]已经判断过,下次判断的话mid-1应该需要在区间内,根据开区间特性,为了让mid-1在区间内的话,那么下次判断区间应该为[left,mid)。
在平时刷题的过程中,我们只需要记住一种方法就好了,像我一般都是直接使用左闭右闭的方法。
版本一:左闭右闭
class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
right=mid-1;
}else{
left=mid+1;
}
}
return -1;
}
}
版本二:左闭右开
class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length;
while(left<right){
int mid =left+(right-left)/2;
if(nums[mid]==target){
return mid;
}else if(nums[mid]>target){
right=mid;
}else{
left=mid+1;
}
}
return -1;
}
}
3.leetcode 27 移除元素
题目链接:https://leetcode.cn/problems/remove-element/
文章链接:https://programmercarl.com/0027.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0.html
视频链接:https://www.bilibili.com/video/BV12A4y1Z7LP/
题目描述:
思路:
因为题目说了必须在原地修改数组,所以不能使用额外的数组来解题。首想的还是暴力,之后发现也能使用双指针。前面数组的特性说到过了,我们只能覆盖元素,不能真正删除该元素的空间。
方法一:暴力法
该题需要移除数组中等于val的元素,我们可以遍历数组,每次碰到val之后,可以将后面的元素全部往前移动。
class Solution {
public int removeElement(int[] nums, int val) {
//解法一:暴力法
//两层for循环,一次遍历整个数组,一次将后面的元素前移
int count=0;//计算val的个数
for(int i=0;i<nums.length-count;i++){
if(nums[i]==val){
for(int j=i+1;j<nums.length-count;j++){
nums[j-1]=nums[j];
}
count++;
i--;//这里i--的意思就是,删除完val之后,后面的元素又覆盖到该位置上了,下次判断还需要从 该位置判断,所以i--
}
}
return nums.length-count;
}
}
方法二:双指针法
原理就是快慢指针,快指针用来遍历整个数组,慢指针用来填充新数组
当快指针遍历到与val不等的时候,那么慢指针的位置就可以填充元素,
当快指针遍历到与val相等的时候,那么快指针会跳过,继续往后遍历,而慢指针不会对其进行填充。
class Solution {
public int removeElement(int[] nums, int val) {
//解法:双指针
//思路:一个指针用来遍历数组,一个指针用来填充数组(覆盖原数组)
//当nums[i]==val时,不会填充,i后移,j不动
//当nums[i]!=val时,填充数组,i往后继续遍历,填充指针j后移一位等待下个元素填充
int i=0,j=0;
for(i=0;i<nums.length;i++){
if(nums[i]!=val){
nums[j++]=nums[i];
}
}
return j;
}
}