Day 1

文章详细阐述了数组的基本概念,包括一维数组、二维数组在C++和Java中的区别,以及二维数组的内存分布。接着讨论了二分查找算法,包括其前提条件和不同区间定义下的实现,强调了循环不变量的重要性。最后介绍了移除元素的两种方法,暴力解法和双指针法,分析了它们的时间复杂度。
摘要由CSDN通过智能技术生成

代码随想录算法训练营Day01 数组理论基础,704. 二分查找,27. 移除元素

目录

数组理论

一.普通数组(一维)

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

算法通关数组.png

2.数组下标都是从0开始的;数组内存空间的地址是连续的。

4.使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。

5.数组的元素是不能删的,只能覆盖。

二.二维数组
1.C++中的二维数组的内存分布是连续的。

image-20230524112130389.png

若有下面数组

int array[2][3] = { {0, 1, 2}, {3, 4, 5} };

则在内存的分布情况是(每次运行程序的内存具体地址可能不同,地址是16进制显示的):

image-20230524112616177.png

								图中是在64位编辑器环境得到的结果

注:若是在32位环境下编译得到的内存地址是32位的

2.java中的二维数组的内存分布是连续的。

​ Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。所以看不到每个元素的地址情况

public static void test_arr() {
    int[][] arr = {{1, 2, 3, 4}, {5,6,7,8}, {9,10,11,12}};
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
}

输出的地址为:

[I@7852e922
[I@4e25154f
[I@70dea4e

注:这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。

此时在java中内存可能的分配情况:

image-20230524113544614.png

3.此外:补充一下C/C++ 与java中创建二维数组需要关注的事情:

在C/C++语言中,二维数组的初始化可以省略行数,但不能省略列数;而在java中却是正好相反的,即列数可以省略,

C/C++

1.在C语言中,二维数组的元素数量等于行数和列数的乘积,所以二维数组一经定义,其被分配的内存大小就已经确定了。因此,对于单纯的定义二维数组来说,其行和列都是不可省略的,一旦有省略,那么在编译时就会报“数组内存大小不确定”的错误。

2.而我们所说的可以省略行数,是在对二维数组初始化的时候,即在定义时给数组元素赋初值的时候。要想编译时不报错,就需要让编译器知道该数组占用的内存空间,只不过在有省略的情况下就只能让编译器自己推断出数组占用的内存空间了,那么为什么省略了行就能够推断出来,而省略了列就不可以呢?

这就要提到二维数组元素在内存中的存储方式了。**C/C++**语言是按照“先行后列”的顺序来存储数组的,即先存储第0行的元素,然后是第1行的元素,以此类推。所以编译器必须知道每行元素的个数,才能由初始化的结果推断出行数,进而推断出二维数组所占用的内存空间。而每行元素的个数正是二维数组的列标。

JAVA

Java语言中对于二维数组的定义稍微有些复杂,创建一个Java数组需要三个步骤:声明数组、创建数组空间、创建数组元素并初始化。

其中初始化可以分为:静态初始化和动态初始化。

静态初始化

int[][] arr = new int[][]{{1,2,3,6},{4,5},{7,8,9}};
等价于
int arr[][] = {{1,2,3,6},{4,5},{7,8,9}};

动态初始化

int [][] arr3 = new int[4][3];
int [][] arr4 = new int[4][];

由动态初始化可以看出,在还没有为二维数组元素赋初值时,列下标是可以省略的。在这里要声明的一点是:C/C++语言中二维数组的每个元素都是大小相同的一维数组,即如果把其中的各个元素铺开,会是一个矩形;但在Java中并不要求每一个一维数组的大小一致,所以也就不能在定义的时候说明列数。

那么Java的二维数组是怎样存储的呢?

Java二维数组的数组名存储在栈中,堆里面存放的是new出来的结构,比如具体的数组元素。在定义二维数组时,先在栈里申请行数,然后等具体要用到哪一个一维数组了再向堆申请内存。

所以在定义二维数组时,若省略了列数,则可以看做是申请了若干个(行数)一维数组,但是具体的一维数组中的数据暂时是不知道的。

下面给出Java中二维数组的内存解析图:

image-20230524114456685.png

由上图可知:数组arr1在定义时行标和列标都给出了,其定义的过程可以描述为:先在栈里为arr1申请行数,即为arr1申请一片空间并把空间的首地址赋给arr1,相当于确定好了该二维数组arr1中有三个元素,分别为三个一维数组。而列标被定义出来就意味着为二维数组的每个数据元素都分配好了内存空间,并把三个一维数组的首地址传了过去。对于arr1的各个数据元素,因为在定义的时候没有赋初值,且是String类型,所以默认为null。

数组arr2在定义时省略了列标,所以相当于只给出了arr2这个int型二维数组的四个一维数组元素,而没有为这四个一维数组赋初值。而因为arr2的四个元素都为引用数据类型(数组),所以默认值为null。

  • arr2[1] = new int[5]; 相当于为arr2的第二个元素指明了一块内存空间,并把这块空间的首地址赋给了arr2[1],arr2[1]的长度为5,元素类型为int型,又因为没有为这个一维数组赋初值,所以默认值为0。
  • arr2[1][1] = 1; 的作用是把arr2[1]这个一维数组的第二个元素赋值为1。
  • arr2[2][2] = 1; 因为没有为arr2的第三个元素分配内存空间,所以此时会报空指针异常。

最后,再次回到Java中定义二维数组时为什么不能省略行数的问题。结合上述的内存解析,我们知道Java中的二维数组是要先确立行数,进而才能确立列数,也就是要申请一片内存空间用来存放每个一位数组的地址,然后才能为每个一维数组分配内存空间。

704. 二分查找

题目链接

使用二分法的前提:力扣704 二分查找题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。

💡思路

要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

由于数组元素为有序排列,我们仅需要确定搜索的左右边界。在该题中存在两种情况,即右边界的选择:

左闭右闭 right = nums.size() - 1;

在这种情况下,每次的搜索区间为 [left, right] 即左闭右闭区间,此时对应的循环条件应为 while (left <= right),终止条件为 left == right + 1,即 [right + 1, right],此时区间为空,故循环终止,程序返回 -1 即可。

每次搜索的区间都是从[left,right]

if (nums[mid] < target) 说明我们下一次循环(搜索)前要将left= mid + 1

使下次在【mid+1,right】的区间搜索,因为本次已经判断mid了。

if (nums[mid] > target) 说明我们下一次循环(搜索)前要将right= mid - 1

使下次在 【left,mid-1】的区间搜索,因为本次已经判断mid了。

当循环结束时,left == right + 1,此时区间范围为【right+1,right】,

未找到目标,返回 -1。

代码实现:

// 左闭右闭区间写法
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};
  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)
  • 时间复杂度: O ( 1 ) O(1) O(1)
左闭右开 right = nums.size();

对于这种情况,由于数组,此时的搜索区间为[left, right)即左闭右开区间,此时对应的循环条件为 while (left < right),终止条件为 left == right,即 [right, right],此时区间内仅存一个元素 right,直接返回 -1 是不对的,还需对该索引进行判断。

每次搜索的区间都是从[left,right)

if (nums[mid] < target) 说明我们下一次循环(搜索)前要将left= mid + 1

使下次在【mid+1,right )的区间搜索,因为本次已经判断mid了。

if (nums[mid] > target) 说明我们下一次循环(搜索)前要将right= mid

使下次在【left,mid)的区间搜索,因为本次已经判断mid了,且右区间是开区间。

当循环结束时,left == right,此时区间为【right,right),

未找到目标,返回 -1。

代码实现:

// 左闭右开区间写法
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};
  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)
  • 时间复杂度: O ( 1 ) O(1) O(1)

上述时间复杂度和空间复杂度概念:

  1. 时间复杂度:时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。

    假设总共有n个元素,每次查找的区间大小就是n, n 2 \frac {n} {2} 2n n 4 \frac {n} {4} 4n,…, n 2 k \frac {n} {{2}^{k}} 2kn,其中k就是循环的次数,
    n 2 k \frac {n} {{2}^{k}} 2kn >= 1(1最坏的情况,即还剩一个元素),
    n 2 k \frac {n} {{2}^{k}} 2kn=1,
    可得k= l o g 2 n {log}^{n}_{2} log2n(以2为底,n的对数),
    所以时间复杂度可以表示O(log2n)。即O(log n)

  2. 空间复杂度:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
    因为变量只创建一次,所以空间复杂度为O(1)。

总结

区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。

27. 移除元素

题目链接

💡思路

暴力解法

这里补充一下删除数组中下标为key的元素方法:

方法一 以i = key + 1为基准:

int main()
{
    vector<int> arr={1,2,3,4,5,6,7,8,9,10};
	int len = arr.size(); // 数组的大小

    int key = 3; //删除下标为key的数组元素 
 	for(int i = key+1; i < len ;i++)//注意i为key+1,是要删除元素下标的后一个,i的最大值为len - 1
    {
        arr[i-1] = arr[i];//移动的时候,将当前i(要删除元素下标的后一个)移动到i-1(要删除元素的下标)
    }

    //打印结果
	for(int i = 0;i<len;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;

    return 0;
}

输出如下:

1 2 3 5 6 7 8 9 10 10 

方法二 以i = key为基准:

int main()
{
    vector<int> arr={1,2,3,4,5,6,7,8,9,10};
	int len = arr.size(); // 数组的大小

    int key = 3; //删除下标为key的数组元素 
 	for(int i = key; i < len-1 ;i++)//注意i为key,是要删除元素的下标,i的最大值为len - 1 - 1即len - 2
    {
        arr[i] = arr[i+1];//移动的时候,将当前i+1(要删除元素下标的后一个)移动到i(要删除元素的下标)
    }

	for(int i = 0;i<len;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;

    return 0;
}

输出如下:

1 2 3 5 6 7 8 9 10 10 

这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。

删除过程如下:

27.移除元素-暴力解法

代码实现:

//这里移动数组选择方法一 以 int j = i + 1为基准
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
                for (int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                i--;    // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
                size--; // 移除要删除的元素后,此时数组的大小-1
            }
        }
        return size;

    }
};
  • 时间复杂度: O (   n 2 ) O(\ n^2) O( n2)
  • 时间复杂度: O ( 1 ) O(1) O(1)
双指针法

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

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置

删除过程如下:

27.移除元素-双指针法

代码实现:

注意这些实现方法并没有改变元素的相对位置!

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        // 快指针:不包含目标元素
        // 慢指针:更新原有数组
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
            if (val != nums[fastIndex]) {
                // 这里等价于nums[slowIndex] = nums[fastIndex]; slowIndex++;
                nums[slowIndex++] = nums[fastIndex];
            }
        }
        return slowIndex;
    }
};
  • 时间复杂度: O (   n ) O(\ n) O( n)
  • 时间复杂度: O ( 1 ) O(1) O(1)

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法

--------------------------------------------------- 分割线-----------------------------------------------------

补充题目:

35.搜索插入位置

题目链接

image-20230525180121335.png

  1. 目标值在数组所有元素之前

  2. 目标值等于数组中某一个元素

  3. 目标值插入数组中的位置

  4. 目标值在数组所有元素之后

💡思路

暴力解法

遍历整个数组:

1.若target大等于当前数组索引的值,有三种情况:情况2目标值等于数组中某一个元素;情况3 目标值插入数组中的位置;对应情况1目标值在数组所有元素之前。

2,若遍历完数组没有找到,有两种情况:情况4目标值在数组所有元素之后。或者 nums为空,则返回nums的长度 (题目明确强调了1 <= nums.length <= 104

代码实现:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        for (int i = 0; i < nums.size(); i++) {
        // 分别处理如下三种情况
        // 目标值在数组所有元素之前
        // 目标值等于数组中某一个元素
        // 目标值插入数组中的位置
            if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
                return i;
            }
        }
        // 目标值在数组所有元素之后的情况
        return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度(当然题目的1 <= nums.length <= 104)
    }
};
  • 时间复杂度: O (   n ) O(\ n) O( n)
  • 时间复杂度: O ( 1 ) O(1) O(1)
二分查找

此题题目中强调了nums无重复元素升序 排列数组,因此可以使用二分查找。

左闭右闭的区间,[left, right]

代码实现:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle;
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前  [0, -1]
        // 目标值等于数组中某一个元素  return middle;
        // 目标值插入数组中的位置 [left, right],return  right + 1
        // 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1 也可以是 return  left
        return right + 1;  // return left; 也可以,因为left此时就等于right + 1
    }
};
  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)
  • 时间复杂度: O ( 1 ) O(1) O(1)
左闭右开的区间,[left, right)

代码实现:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n; // 定义target在左闭右开的区间里,[left, right)  target
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在 [middle+1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前 [0,0)
        // 目标值等于数组中某一个元素 return middle
        // 目标值插入数组中的位置 [left, right) ,return right 即可
        // 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
        return right; // return left; 也是可以的,因为此时left 等于right 
    }
};
  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)

  • 时间复杂度: O ( 1 ) O(1) O(1)

总结

确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。然后在二分查找的循环中,坚持循环不变量的原则

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值