个人博客主页:http://myblog.nxx.nx.cn
代码GitHub地址:https://github.com/nx-xn2002/Data_Structure.git
Day1
数组理论基础
定义:数组是存放在连续内存空间上的相同类型数据的集合
重点:
- 数组下标都是从0开始的
- 数组内存空间的地址是连续的
二分查找
704. 二分查找
LeetCode题目链接:
https://leetcode.cn/problems/binary-search/
题目描述:
给定一个 n
个元素有序的(升序)整型数组 nums
和一个目标值 target
,写一个函数搜索 nums
中的 target
,如果目标值存在返回下标,否则返回 -1
。
思路:
题目中强调了输入的数组是有序且元素都不重复,因此结合题目需求,我们可以优先考虑使用二分法解决,而二分法又根据有效区间划分,常见有左闭右闭和左闭右开两种形式,以下是实现代码:
左闭右闭
/**
* 左闭右闭
*
*/
public int search(int[] nums, int target) {
int left = 0, 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;
}
- 时间复杂度:
O(logN)
- 空间复杂度:
O(1)
左闭右开
/**
* 左闭右开
*
*/
public int search(int[] nums, int target) {
int left = 0, 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;
}
- 时间复杂度:
O(logN)
- 空间复杂度:
O(1)
重点
- mid 中间点的计算:
int
类型占用 4 字节,32 比特,即 int
类型的数据范围是:[-2^31 ~ 2^31-1] ,因此当数组数据量较大时,此时 left + right
的结果可能会超出 int
类型的范围,发生溢出问题。因此如果使用以下写法会出现问题:
int mid = (left + right) / 2;
正确写法:
int mid = left + (right - left) / 2;
- 二分查找的前置条件:
二分法基于分治思想,而通过具体的代码实现,我们也可以注意到,要想实现二分算法,前置条件就是相关序列必须是有序的
- 区间和边界处理:
我们可以看到,二分法可以选择左闭右闭和左闭右开两种写法,而这两种写法中,核心的区别就在于对各种边界条件的处理上。我认为最简单的理解记忆方式就是根据不同区间下,left
和 right
的有效值范围来理解。
比如,对于 while
循环的终止条件,在左闭右闭的情况下,left
取值为 right
的时候,值依然是在有效区间内,而左闭右开时 left
取值为 right
的时候,left
的值无效,因此分别是使用 <=
和 <
作为终止条件,而循环内的 if
语句也是同理。
移除元素
LeetCode题目链接:
https://leetcode.cn/problems/remove-element/
题目描述:
给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要 - 返回
k
思路:
我们可以很轻松地想到暴力解法,即遍历数组的同时进行判断,如果出现需要移除的元素,则通过循环将该元素后的所有元素前移一位即可,同时维护一个变量记录数组长度就能满足所有需求。这样做能够通过力扣,但是算法的时间复杂度是 O(N^2)
,还有更简便的思路,就是双指针(快慢指针)法。
在双指针法中,我们只需要维护快慢两个指针的位置。通过快指针去对数组进行遍历操作,慢指针来标记当前最高有效位的下一位,然后把遍历到的有效元素存入到慢指针指向的位置即可,这种算法因为避免了数组元素批量移动时的遍历操作,时间复杂度是 O(N)
。
暴力法
/**
* 暴力解法
*/
public int removeElement1(int[] nums, int val) {
int len = nums.length;
for (int i = 0; i < len; i++) {
if (nums[i] == val) {
for (int j = i + 1; j < len; j++) {
nums[j - 1] = nums[j];
}
i--;
len--;
}
}
return len;
}
- 时间复杂度:
O(N^2)
- 空间复杂度:
O(1)
双指针(快慢指针)法
/**
* 双指针(快慢指针)
*/
public int removeElement2(int[] nums, int val) {
int slow = 0, fast = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
- 时间复杂度:
O(N)
- 空间复杂度:
O(1)
重点
这部分知识点相对来说比较简单,通常情况下,暴力解法也能满足需求,但是当出现重复遍历的情况时,其实可以考虑一些常见的优化方式,如这里的双指针法。