前提:这是我第一次写博客,写的不好的地方请多多指教,谢谢包容
数组理论基础
由于很久没有接触过数据结构了,首先先回顾一下数组的理论。其实看着看着之前的知识慢慢回忆起来了。
-
对于一维数组来说,其实就是内存给你开了一个连续的地方让你存放数据。我们所定义数组名字其实就是一个指针,例如
a[5] = {1, 2, 3, 4, 5}
实际上a就是一个数组指针,指向了这个数组的第一个数据,也就是 1 ,我们可以尝试输出一下*a
的值就可以看到*a = 1
,a[0]
其实就是*(a + 0)
这就证明我的想法是对的,对于数组的读取,除了用基本的a[0]
之外,我们还能通过使用*(a + n)
来访问数组,这里得到n值就是我们要读取的位数 - 1。 -
对于二维数组来说,其实理解上是一样的,二维数组的存放方式也是像一维数组一样连续(C++,其他语言不知道)。这时候,我们所定义的数组名字就是一个二维指针,也就是指针的指针,这时候如果我们直接输出
*a
的值就会发现,他输出的是一串十六进制地址,实际上他输出的是我们啊a[0][0]
这一头元素的地址,如果我们不适用基本的a[n][m]
去读取数据,而是使用指针去操作元素的话,我们可以分别用指针指向二维元素或者作为一个数组指针指向一维数组。#include <stdio.h> int main( ) { int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23}; int *p; for(p=a[0];p<a[0]+12;p++) { printf("%3d ",*p); } return 0; }
像这里我们用p去指向数组里的每一个元素,另一种方法如下所示:
#include <stdio.h> int main( ) { int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23}; int i,j,(*p)[4];//定义一个指向有四个元素的一维数组的指针 for(p=a,i=0;i<3;i++,p++) //i用于计算行数,这里的p++一次并不是加 1,而是加你所定的列数,像这里就是加 4(每一行有4列,p++会直接跳到下一行) { for(j=0;j<4;j++) { printf("%3d ",*(*p+j));//!!尤其注意,这里的p存储的是a行整体的地址指向一维数组a[0]整体,而不是指向一个具体的值! //*p的值取出来的值就是a[0][0]的地址,*p+j就是存储a[0][j]行的地址, // *(*p+j)就是该地址指向的元素的值!! } printf("\n"); } return 0; }
当你能看懂二维数组的时候,高维数组也是同样的道理,也是在二维数组上进行堆叠就好了。
LeetCode 704.二分查找
因为这道题目之前的时候看过,所以在写的时候并没有什么迷茫的地方,最主流的写法是左闭右闭与左闭右开的两种写法,看了视频连接之后理解得更加深刻。
这道题的要点就是判断while()循环里面的是 Left < Right 还是 Left <= Right,以及在更改左右区间的值时,Left要不要+1以及Right要不要-1。要解决这些问题,我们首先要看我们所定义的区间类型是属于哪一种,在左闭右闭的时候,Left是可以等于Right的,在区间定义上使合法的,此时我们就可以使用Left <= Right;相应的,在左闭右开的时候,Left等于Right会使区间定义违法,此时只能用Left < Right。
然后就是更改左右区间的情况,这个我跟视频的理解有些不同,更改左右区间的值,都是防止要查找的Target在数组两端而出现问题。当时左闭右闭时,此时更新的时候就不应该再次包含Mid这个数,而是将右区间定义在Mid的前一位或者后一位(看Target是大于Mid还是小于Mid),不然就会产生重复,如图所示,此时的查找区间就是位号为0,1,2这几个数,如果右区间没有减一,此时会变成位号为0,1,2,3这几个数,会多一个数(4),但是这个数我们已经判断过了,就会形成重复判断。
而左闭右开时,这时候更新右区间的值时,就应该将右区间放在Mid的位置上,如图所示,此时的查找区间应该为位号是0,1,2这几个数,这样查找区间就正确了,要是右区间继续时Mid-1,这时候的查找区间就是0,1这两个数,会少了位号为2的这个数(3)。更新左区间时依然按照上面左闭右闭的情况进行Mid+1就行了。但是此时要注意,Right开始的时候应该为数组的长度n,因为是右开,Right如果是n-1的话,就会使数组最右边的数给排除掉,造成查找错误。
下面是具体实现代码:
- 左闭右闭
class Solution {
public:
int search(vector<int>& nums, int target) {
//左闭右闭
int n = nums.size();
int Left = 0, Right = n - 1;
while(Left <= Right)// 当Left = Right时此时区间有意义可以取到
{
int Mid = (Left + Right) / 2;
if(target < nums[Mid])
{
Right = Mid - 1;
}
else if(target > nums[Mid])
{
Left = Mid + 1;
}
else return Mid;
}
return -1;
}
};
- 左闭右开
class Solution {
public:
int search(vector<int>& nums, int target) {
//左闭右开
int n = nums.size();
int Left = 0, Right = n;// 因为不包含右区间,所以此时的Right应该为最后一个成员的后一位,及(n-1)+1
while(Left < Right)// 当Left = Right时此时区间没有意义,所以不可以取到
{
int Mid = (Left + Right) / 2;
if(target < nums[Mid])
{
Right = Mid;// 因为此时的右区间为开,所以此时不需要—1,直接就是等于Mid就好了,下一次循环会在Mid的前一位开始寻找
}
else if(target > nums[Mid])
{
Left = Mid + 1;// 此时的左区间位闭,所以此时需要+1,将Mid这个成员排除掉,避免他进入下一次循环
}
else return Mid;
}
return -1;
}
};
另外,我自己也按照尝试写了一下左开右闭的,但是发现按照上面的理解会出现问题。当数组为{-1, 0, 3, 5, 9, 12},Target为2时会进入死循环,此时Left=0,Right = 1卡死,我暂时还没想到是哪里出了问题,各路大神要是看到了可以帮忙指点一下我。
class Solution {
public:
int search(vector<int>& nums, int target) {
//左开右闭
int n = nums.size();
int Left = -1, Right = n - 1;
while (Left < Right)
{
int Mid = (Left + Right) / 2;
if (target < nums[Mid])
{
Right = Mid - 1;
} else if (target > nums[Mid])
{
Left = Mid;
} else return Mid;
}
return -1;
}
};
找到问题的出处了,当我们采取左开右闭的时候,容易面对一个情况就是(n,n+1],这时候的``Mid = (n + n + 1) = n,然后就会无限等于n从而进入死循环,这时候我们要修改Mid的值,将Mid修改为
Mid = (Left + Right) / 2 + 1`,这样子就能解决问题。
然后当你拿着这个修改了的代码去提交时,发现又错了。此时会出现两种情况,第一种是如果当数组只有一个数的时候,你的Mid =(-1 + 0)/ 2 + 1 = 1
,就会访问越界了;第二种情况就是,当target为数组第一个元素的时候,Right最后都会等于0,但此时的Mid =(-1 + 0)/ 2 + 1 = 1
,又进入了一个新的死循环。
观察这两种问题,都归结于计算的公式,因为公式在计算是会把-0.5给约成0而不是-1,所以我们是没法取到位号为0的元素,我们修改一下公式,把他从Mid = (Left + Right) / 2
变成Mid = Left + (Right - Left) / 2 + 1
,此时就能使Mid可以为0,不会进入到死循环的一个状态。而且这个公式还有一个好处就是能够防止两个大数相加导致超出int的范围,一石二鸟之妙。
下面是正确的代码:
class Solution {
public:
int search(vector<int> &nums, int target)
{
//左开右闭
int n = nums.size();
int Left = -1, Right = n - 1;
while (Left < Right)
{
int Mid = Left + (Right - Left) / 2 + 1; //注意Mid公式的转换
if (target < nums[Mid])
{
Right = Mid - 1;
}
else if (target > nums[Mid])
{
Left = Mid;
}
else return Mid;
}
return -1;
}
};
LeetCode 27.移除元素
第一种就是暴力解法,遍历整个数组,遇到匹配的直接让后面的数覆盖掉前面的,但是这种时间复杂度为O(n²),下面的双指针法会更加合适
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
int count = 0;
for (int i = 0; i < n - count; i++)
{
if (nums[i] == val)
{
count++;
for (int j = i; j < n - 1; j++)
{
nums[j] = nums[j + 1];
}
i--;
}
}
return n - count;
}
};
第二种就用到了双指针,一快一慢,一开始,两个指针同步向前,当匹配到目标时,快指针向前一步,慢指针原地不动,知道快指针所指的数不是目标,此时慢指针所指的数换为快指针所指的数,然后慢指针也随同快指针一起向前,知道下一次匹配到目标。看文章连接的动图会更加清晰明了。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
int Slow = 0, Fast = 0;
for (; Fast < n; Fast++)
{
if (nums[Fast] != val)
{
nums[Slow++] = nums[Fast];
}
}
return Slow;
}
};