数据结构与算法-数组,二分查找及其力扣相关算法题(配套代码随想录)

数组入门介绍

  1. 数组在内存中的存储方式:
    数组是存放在连续内存空间上的相同类型数据的集合。
    字符数组
    特点
    • 数组下标都是从0开始的
    • 数组内存空间的地址是连续的, 且元素类型相同
    • 数组的元素是不能删的, 只能覆盖
  2. 二维数组在内存的空间地址
    • 不同编程语言的内存管理是不一样的
      java中二维数组的内存管理
  3. 数组在操作上的优点及其局限性
    • 优点
      • 空间效率高:数组为数据分配了连续的内存块,无须额外的结构开销。
      • 支持随机访问:数组允许在 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. 螺旋遍历二维数组

思路

  • 59
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1. 基础知识扎实:首先要打好计算机基础知识的基础,如数据类型、算法思想、时间复杂度等等。建议通过课程、书籍等途径系统地学习。 2. 刻意练习:对于数据结构与算法来说,刻意练习是很重要的。可以通过刷、参加比赛等方式来提高自己的水平。 3. 多做笔记:学习过程中,多做笔记可以帮助巩固知识点,方便日后复习回顾。 4. 参加社区:在社区中可以与其他学习者交流,分享自己的学习心得,也可以从其他人的经验中学习到更多的知识。 5. 不断学习:数据结构与算法是一个不断学习的过程,需要不断地学习新的知识点,更新自己的知识储备。 关于如何正常使用力扣,可以参考以下几点: 1. 从简单到复杂:在刷的过程中,建议从简单的目开始,逐步提高难度,这样可以让自己逐渐适应力扣目难度。 2. 掌握基础知识:在刷之前,先掌握一些基础的算法数据结构知识,这样可以更好地理解目,提高解效率。 3. 多看解:在力扣上,每个目都有很多人提交过自己的解答,可以多看一些高赞的解,从中学习新的解思路。 4. 坚持刷:在力扣上,坚持刷是非常重要的,可以通过每天刷一定数量的目来提高自己的水平。 5. 不要过于依赖代码:在刷的过程中,不要过于依赖他人的代码,要尽可能地自己思考和解决问,这样可以更好地提高自己的解能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值