数据结构与算法
数组入门介绍
- 数组在内存中的存储方式:
数组是存放在连续内存空间上的相同类型数据的集合。
特点- 数组下标都是从0开始的
- 数组内存空间的地址是连续的, 且元素类型相同
- 数组的元素是不能删的, 只能覆盖
- 二维数组在内存的空间地址
- 不同编程语言的内存管理是不一样的
- 不同编程语言的内存管理是不一样的
- 数组在操作上的优点及其局限性
- 优点
- 空间效率高:数组为数据分配了连续的内存块,无须额外的结构开销。
- 支持随机访问:数组允许在 O(1) 时间内访问任何元素。
- 缓存局部性:当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。
- 局限性
- 插入与删除效率低:当数组中元素较多时,插入与删除操作需要移动大量的元素。
- 长度不可变:数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大
- 空间浪费:如果数组分配的大小超过实际所需,那么多余的空间就被浪费了。
- 优点
算法
二分法
常见写法
- 第一种写法 (左闭右闭)
/* 二分查找(双闭区间) */
int binarySearch(int[] nums, int target) {
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
int i = 0, j = nums.length - 1;
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中
j = m - 1;
else // 找到目标元素,返回其索引
return m;
}
// 未找到目标元素,返回 -1
return -1;
}
- 第二种写法 (左闭右开)
/* 二分查找(左闭右开区间) */
int binarySearchLCRO(int[] nums, int target) {
// 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
int i = 0, j = nums.length;
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
while (i < j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中
i = m + 1;
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
j = m;
else // 找到目标元素,返回其索引
return m;
}
// 未找到目标元素,返回 -1
return -1;
}
二分查找插入点
- 无重复元素
/* 二分查找插入点(无重复元素) */
int binarySearchInsertionSimple(int[] nums, int target) {
int i = 0, j = nums.length - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入点 m
}
}
// 未找到 target ,返回插入点 i
return i;
}
- 有重复元素
/* 二分查找插入点(存在重复元素) */
int binarySearchInsertion(int[] nums, int target) {
int i = 0, j = nums.length - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中
}
}
// 返回插入点 i
return i;
}
二分查找边界
- 查找左边界
/* 二分查找最左一个 target */
int binarySearchLeftEdge(int[] nums, int target) {
// 等价于查找 target 的插入点
// 该方法在上个模块二分查找插入点的有重复元素情况
int i = binary_search_insertion.binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.length || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
- 查找右边界
/* 二分查找最右一个 target */
// 复用查找左边界
int binarySearchRightEdge(int[] nums, int target) {
// 转化为查找最左一个 target + 1
int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);
// j 指向最右一个 target ,i 指向首个大于 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
- 转化为查找元素 (可查找左右边界)
/**
* 将查找左右边界转化为元素
* 当数组不包含 target 时,最终 i 和 j 会分别指向首个大于、小于 target 的元素。
*
* @param nums
* @param target
* @return
*/
int binarySearchRightEdge(int[] nums, int target) {
// 查找最左一个target
double newTarget = target - 0.5;
/*查找最右一个target
double newTarget = target + 0.5;*/
//复用二分查找
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
int i = 0, j = nums.length - 1;
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < newTarget) // 此情况说明 target 在区间 [m+1, j] 中
{
i = m + 1;
}
else if (nums[m] > newTarget) // 此情况说明 target 在区间 [i, m-1] 中
{
j = m - 1;
}
else // 找到目标元素,返回其索引
{
return m;
}
}
// 查找左边界
return i;
/*查找右边界
return j; */
}
二分查找的优点和局限性
- 优点
- 二分查找在时间和空间方面都有较好的性能。
- 二分查找的时间效率高。在大数据量下,对数阶的时间复杂度具有显著优势。
- 二分查找无须额外空间。相较于需要借助额外空间的搜索算法(例如哈希查找),二分查找更加节省空间。
- 局限性
- 二分查找仅适用于有序数据。
- 二分查找仅适用于数组。二分查找需要跳跃式(非连续地)访问元素,而在链表中执行跳跃式访问的效率较低,因此不适合应用在链表 或基于链表实现的数据结构。
- 小数据量下,线性查找性能更佳。
双指针法
快慢指针法
例题详解 Ⅰ : 27. 移除元素
相向双指针法
例题详解 Ⅰ : 27. 移除元素
例题详解 Ⅱ : 977. 有序数组的平方
滑动窗口
例题详解 Ⅰ : 209. 长度最小的子数组
模拟行为
例题详解 Ⅰ : 59. 螺旋矩阵 Ⅱ
力扣题目
704. 二分查找
题目链接 : 704.二分查找
class Solution {
// 左闭右闭
public int binarySearch(int[] nums, int target) {
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
int i = 0, j = nums.length - 1;
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中
j = m - 1;
else // 找到目标元素,返回其索引
return m;
}
// 未找到目标元素,返回 -1
return -1;
}
}
思路
此题目与 二分查找 中的两种写法解法相同, 不再赘述
代码随想录提供思路 : 代码随想录
35. 搜索插入位置
题目链接 : 35. 搜索插入位置
class Solution {
public int searchInsert(int[] nums, int target) {
// 初始化 i j 指向数组两端(左闭右闭)
int i = 0;
int j = nums.length - 1;
// 思路基本与二分查找类似
while (i <= j) {
int m = i + (j - i) / 2;
if (nums[m] < target) {
i = m + 1;
} else if (nums[m] > target) {
j = m - 1;
} else {
return m;
}
}
// 查找不到时 i 最后会指向比target大的第一个元素 数值上相当于插入后的索引
return i;
}
}
思路
1. 当数组中存在该元素时, 二分查找返回该元素的索引
2. 当数组中不存在该元素时, 二分查找结束后 i 将会指向比 target 大的第一个索引, 这个索引的值就是该元素的应插入位置的索引值
代码随想录提供思路 : 代码随想录
34. 在排序数组中查找元素的第一个和最后一个位置
题目链接 : 34. 在排序数组中查找元素的第一个和最后一个位置
思路
代码随想录提供思路 : 代码随想录
69. x 的平方根
题目链接 : 69. x 的平方根
思路
367. 有效的完全平方数
题目链接 : 367. 有效的完全平方数
思路
27. 移除元素
题目链接 : 27. 移除元素
快慢指针法
class Solution {
public int removeElement(int[] nums, int val) {
// 慢指针
int slowIndex = 0;
// 快指针向前循环查找不为val的数
for(int fastIndex = 0; fastIndex < nums.length; fastIndex++){
// 查找到之后将此数字放到慢指针对应的索引上, 同时慢指针向前移动一位
if(nums[fastIndex] != val) {
nums[slowIndex++] = nums[fastIndex];
}
}
// 循环结束后慢指针刚好向前移动了一位,数值等于数组长度
return slowIndex;
}
}
思路
1. 快指针向前循环, 找到不是val的数后放在慢指针对应的索引上, 慢指针向前移动一位
2. 循环结束后慢指针的索引数值等于数组长度
双向指针法
class Solution {
public int removeElement(int[] nums, int val) {
// i j 为数组两边边界
int i = 0, j = nums.length - 1;
// 将 j 移到从右边第一个不为 val 的索引
// 与放在下方while中相比, 减少循环判断次数
while (j >= 0 && nums[j] == val) {
j--;
}
// 大循环 将左边等于val的索引i的值替换为右边不为val的索引j的值
while (i <= j) {
if (nums[i] == val) {
nums[i++] = nums[j--];
}
// 小循环 使右边索引j的值始终不为val
while (j >= 0 && nums[j] == val) {
j--;
}
}
// 循环后整体结束 此时i为数组长度, j+1也是
return i;
}
}
思路
1. i 从左边向右遍历, j 从右边向左遍历, nums[i] = val时, 替换为不为val的nums[j]
2. 每次大循环一次后, i 都指向下一个值, 并不知道此值是否等于 val
3. 若 i 此时指向的值为 val, 则小循环结束后 j = i - 1, 大循环结束, 数组最大索引为 j, 数组长度为 j + 1 = i
4. 若 i 此时指向的值不为 val, 则小循环结束后 j >= i, 大循环最终停止后数组长度为i
代码随想录提供思路 : 代码随想录
26. 删除有序数组中的重复项
题目链接 : 26. 删除有序数组中的重复项
思路
283. 移动零
题目链接 : 283. 移动零
思路
844. 比较含退格的字符串
题目链接 : 844. 比较含退格的字符串
思路
977. 有序数组的平方
题目链接 : 977. 有序数组的平方
class Solution {
public int[] sortedSquares(int[] nums) {
// left right 指向正负数两边界 各为正数和负数平方最大的值
int left = 0, right = nums.length - 1;
// 定义新数组接收
int[] result = new int[nums.length];
// 新数组的最大索引 循环中从大到小插入数字
int index = result.length - 1;
// 双指针对比插入数字,数字大的插入成功,成功后的指针移动,移动后的指针与对面的旧指针做对比
while (left <= right) {
if (nums[left] * nums[left] < nums[right] * nums[right]) {
result[index--] = nums[right] * nums[right--];
} else {
result[index--] = nums[left] * nums[left++];
}
}
return result;
}
}
思路
1. 双指针在数组两边, 对比平方后值的大小, 值大的插入新数组, 指针往内移动
2. 指针移动后与零一边未移动的指针做对比, 重复循环此操作
3. 两指针指向同一个数, 此数插入0索引, 之后指针移动, 不再满足循环条件, 退出循环
代码随想录提供思路 : 代码随想录
209. 长度最小的子数组
题目链接 : 209. 长度最小的子数组
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// 滑块的左端
int left = 0;
// 滑块内数字的和
int sum = 0;
// 最短的滑块, 初始最大值使下面min好判断
// 思想 : 要找最小值, 可以初始化为最大值然后min方法求小
int result = Integer.MAX_VALUE;
// for循环滑块右端右移
for (int right = 0; right < nums.length; right++) {
// 右移后添加数字到总和sum
sum += nums[right];
// 当总和sum >= target 时, 循环去掉左端并减少总和, 直到sum再次不满足条件
while (sum >= target) {
// 满足题目条件并且在去掉左端之前更新结果
result = Math.min(result, right - left + 1);
sum -= nums[left++];
}
}
// 添加对初始值的判断
return result == Integer.MAX_VALUE ? 0 : result;
}
}
思路
1. 设置滑块, 左右端初始值都为 0 索引, 随后右端向右移直到总和满足题干要求
2. 满足要求后循环去掉左端直到刚好不再满足题干要求, 此时继续右端右移
3. 循环操作直到滑块到达最右端不能再移动为止
4. 最后要求得滑块的最小值, 要用min函数, 因此初始化不能为 0 而是整数的最大值, 从而使min函数可以一直被使用, 结果要进行初始值的判断
代码随想录提供思路 : 代码随想录
904. 水果成篮
题目链接 : 904. 水果成篮
思路
76. 最小覆盖子串
题目链接 : 76. 最小覆盖子串
思路
59. 螺旋矩阵 Ⅱ
题目链接 : 59. 螺旋矩阵 Ⅱ
class Solution {
public int[][] generateMatrix(int n) {
int loop = 0; // 循环次数
int[][] result = new int[n][n]; // 结果二维数组
int count = 1; // 设置添加入数组的递增的数值
int i, j; // 循环内所需, 代表数组的行数 列数
int start = 0; // 每次循环的起始索引
// 循环每一圈添入数字
while (loop++ < n / 2) {
// 数组上侧数值
for (j = start; j < n - loop; j++) {
result[start][j] = count++;
}
// 数组右侧数值
for (i = start; i < n - loop; i++) {
result[i][j] = count++;
}
// 数组下侧数值
for (; j >= loop; j--) {
result[i][j] = count++;
}
// 数组左侧数值
for (; i >= loop; i--) {
result[i][j] = count++;
}
start++;
}
// 循环结束后, 判断该二维数组是否有最中间的一位没有填入数字
// 若有, 则此时start刚好是那个空位的索引, 判断为true
// 若没有, 则数组已在循环中填满, 此判断为false
if (n % 2 == 1) {
result[start][start] = count;
}
// 返回结果数组
return result;
}
}
思路
1. 循环每一圈填入数字, 确认每次循环的起始索引和终止位置
2. 若n为奇数, 则有最中间一空位在循环中未填入数字, 需要判断并手动填入
代码随想录提供思路 : 代码随想录
54. 螺旋矩阵
题目链接 : 54. 螺旋矩阵
思路
LCR 146. 螺旋遍历二维数组
题目链接 : LCR 146. 螺旋遍历二维数组
思路