数组概念
数组可以说是最基础、最简单的数据结构了。数组用一块连续的内存空间,来存储相同类型的一组数据,最大的特点就是支持随机访问,但插入、删除操作也因此变得比较低效,平均情况时间复杂度为 O(n)。在平时的业务开发中,我们可以直接使用编程语言提供的容器类,但是,如果是特别底层的开发,直接使用数组可能会更合适。
为什么大多数编程语言中,数组要从 0 开始编号,而不是从 1 开始呢?
从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用 a 来表示数组的首地址,a[0]就是偏移为 0 的位置,也就是首地址,a[k]就表示偏移 k 个 type_size 的位置,所以计算 a[k]的内存地址只需要用这个公式:
a[k]_address = base_address + k * type_size
但是,如果数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:
a[k]_address = base_address + (k-1)*type_size
对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。
总结:个人理解是因为计算机底层一开始定义的是从0进行,后续产生的编程语言在一定程度上都沿用了该方式。
数组是如何实现根据下标随机访问数组元素?
我们拿一个长度为 10 的 int 类型的数组 int[] a = new int[10]来举例。计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。
我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:
a[i]_address = base_address + i * data_type_size
其中 data_type_size 表示数组中每个元素的大小。我们举的这个例子里,数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。
问数组和链表的区别?
- 链表适合插入、删除,时间复杂度 O(1);
- 数组是适合查找操作,支持随机访问,根据下标随机访问的时间复杂度为 O(1);
插入操作的时间复杂度分析
假设数组的长度为 n,现在,如果我们需要将一个数据插入到数组中的第 k 个位置。为了把第 k 个位置腾出来,给新来的数据,我们需要将第 k~n 这部分的元素都顺序地往后挪一位。那插入操作的时间复杂度是多少呢?
如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。 因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+…n)/n=O(n)。
如果数组中的数据是有序的,我们在某个位置插入一个新的元素时,就必须按照刚才的方法搬移 k 之后的数据。但是,如果数组中存储的数据并没有任何规律,数组只是被当作一个存储数据的集合。在这种情况下,如果要将某个数据插入到第 k 个位置,为了避免大规模的数据搬移,我们还有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放入第 k 个位置。
删除操作的时间复杂度分析
跟插入数据类似,如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。
如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。
实际上,在某些特殊场景下,我们并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,删除的效率是不是会提高很多呢?假如数组 a[10]中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 三个元素。
为了避免 d,e,f,g,h 这几个数据会被搬移三次,我们可以先记录下已经删除的数据。每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。其实这个就是JVM 标记清除垃圾回收算法的核心思想。
在项目开发中,什么时候适合用数组,什么时候适合用容器呢?
以java为例,ArrayList 最大的优势就是可以将很多数组操作的细节封装起来。比如前面提到的数组插入、删除数据时需要搬移其他数据等。另外,它还有一个优势,就是支持动态扩容。
数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。如果我们申请了大小为 10 的数组,当第 11 个数据需要存储到数组中时,我们就需要重新分配一块更大的空间,将原来的数据复制过去,然后再将新的数据插入。
如果使用 ArrayList,我们就完全不需要关心底层的扩容逻辑,ArrayList 已经帮我们实现好了。每次存储空间不够的时候,它都会将空间自动扩容为 1.5 倍大小。不过,这里需要注意一点,因为扩容操作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小。如果不指定数据大小,默认构造一个初始容量为10的空列表。
List<User> users = new ArrayList(10000);for (int i = 0; i < 10000; ++i) {users.add(xxx);}
此处如果没有事先指定数据大小,则会频繁的进行扩容的操作,是会比较消耗性能和提高时间复杂度的。
以下的场景比较适合使用数组
- 1.Java ArrayList 无法存储基本类型
比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
- 2.如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
总的来说数组是一种基本的数据结构,而平时使用的list等语言中的数据类型属于对其进行的封装,也称为容器,容器会帮助开发者自动实现一些功能去实现对数组的操作。
在平时工作中,对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。
数组习题:
1、删除排序数组中的重复项
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
代码片段
/*** 删除排序数组中的重复项* 给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。* 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。* <p>* 作者:华星详谈*/private static int deleteArrayDuplicates(int[] nums) {//思路:使用双指针的方式//定义一个慢指针int k = 0;//迭代循环快指针for(int i=1;i<nums.length;i++){//如果快指针的值和慢指针的值不相等则说明为不重复的,则把值写入慢指针对应的数组元素中去if(nums[i] != nums[k]){k++;nums[k]=nums[i];}}//移动慢指针return k+1;}
运行代码
public static void main(String[] args) {//---------------- 习题1:删除排序数组中的重复项 begin -----------------------------/*** 删除排序数组中的重复项* 示例 1:给定数组 nums = [1,1,2],函数应该返回新的长度 2,* 并且原数组 nums 的前两个元素被修改为 1, 2。你不需要考虑数组中超出新长度后面的元素。* 示例 2:给定 nums = [0,0,1,1,1,2,2,3,3,4],* 函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。你不需要考虑数组中超出新长度后面的元素。*/int[] nums = {1, 1, 2};int length = deleteArrayDuplicates(nums);System.out.println("习题1:新的数组长度为 = " + length);System.out.println("习题1:删除排序数组中的重复项: " + JSONObject.toJSONString(Arrays.copyOfRange(nums, 0, length)));//----------------- 习题1:删除排序数组中的重复项 end ------------------------------}
习题1分析:
- 时间复杂度:O(n)。n为数组长度
- 空间复杂度:O(1)。没有创建额外的空间
2、买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1: 输入: [7,1,5,3,6,4] 输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3
示例 2:输入: [1,2,3,4,5] 输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
示例 3:输入: [7,6,4,3,1] 输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
注意: 你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
Java代码
/*** TODO 买卖股票的最佳时机* 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。* 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。* 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。* <p>* 作者:华星详谈** @param prices* @return*/public static int maxProfit(int[] prices) {//解题思路:使用双指针,a为当前指针,b为下一个指针。//当b>a 时,买入股票//当b<a时,卖出股票,利润=arr[a]-买入时的价格//下一个指针int b = 1;//利润int profit = 0;//买入时的价格int buying = 0;//是否有买入boolean isBuying = false;for (int a = 0; a < prices.length; a++) {if (a == prices.length - 1) {if (isBuying) {profit += prices[a] - buying;}break;}//买入if (!isBuying && prices[b] > prices[a]) {buying = prices[a];isBuying = true;}//卖出if (isBuying && prices[b] < prices[a]) {profit += prices[a] - buying;//清空数据buying = 0;isBuying = false;}b++;}return profit;}
运行代码
int[] nums2 = {7, 1, 5, 3, 6, 4};System.out.println("习题2:买卖股票的最佳时机: " + ArrayAlgorithmToPractice.maxProfit(nums));
习题2分析:
上述代码我们可以看到还存在很大的优化空间,下面给大家带来一种优化后的代码
public static int maxProfit2(int[] prices) {//利润int profit = 0;for (int i = 1; i < prices.length; i++) {//获取利润,当prices[i] - prices[i-1] > 0 时,说明有利可图profit += Math.max(0, prices[i] - prices[i - 1]);}return profit;}
- 时间复杂度:O(n)。其中 n 为数组的长度。我们只需要遍历一次数组即可。
- 空间复杂度:O(1)。只需要常数空间存放若干变量。
习题3、旋转数组
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
说明:
- 尽可能想出更多的解决方案。
- 要求使用空间复杂度为 O(1) 的 原地 算法。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3输出: [5,6,7,1,2,3,4]解释:向右旋转 1 步: [7,1,2,3,4,5,6]向右旋转 2 步: [6,7,1,2,3,4,5]向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2输出:[3,99,-1,-100]解释:向右旋转 1 步: [99,-1,-100,3]向右旋转 2 步: [3,99,-1,-100]
习题3方式一:使用最简单的方法,直接旋转
/*** TODO 旋转数组 方式1:直接旋转* 给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。* 说明:* 尽可能想出更多的解决方案。* 要求使用空间复杂度为 O(1) 的 原地 算法。* <p>* 作者:华星详谈** @param nums* @param k*/public static void rotate1(int[] nums, int k) {//temp:临时存储,previous:之前的值int temp, previous;for (int i = 0; i < k; i++) {//保存数组最后一个元素previous = nums[nums.length-1];for (int j = 0; j < nums.length; j++) {//把当前位的值先暂时放入temp中temp = nums[j];//当前位的值替换为最后位的值nums[j] = previous;//把当前位的值最为下一次循环的最后位值previous = temp;}}System.out.println("方式一 直接旋转:nums = " + JSON.toJSONString(nums));}
运行代码
//方式1:直接旋转nums = new int[]{1, 2, 3, 4, 5, 6, 7};ArrayAlgorithmToPractice.rotate1(nums,3);
习题3方式一 复杂度分析
- 时间复杂度:O(n k)。两个循环体,循环次数不同,故是O(n k)。
- 空间复杂度:O(1)。常数位的空间复杂度,没有额外的空间被使用。
习题3方式二:反转数组
这个方法基于这个事实:当我们旋转数组 k 次,k%n 个尾部元素会被移动到头部,剩下的元素会被向后移动。
在这个方法中,我们首先将所有元素反转。然后反转前 k 个元素,再反转后面 n-k 个元素,就能得到想要的结果。
假设 n=7 且 k=3。
原始数组 : 1 2 3 4 5 6 7反转所有数字后 : 7 6 5 4 3 2 1反转前 k 个数字后 : 5 6 7 4 3 2 1反转后 n-k 个数字后 : 5 6 7 1 2 3 4 --> 结果
/*** TODO 旋转数组 方式2:反转数组* 给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。* 说明:* 尽可能想出更多的解决方案。* 要求使用空间复杂度为 O(1) 的 原地 算法。* <p>* 作者:华星详谈** @param nums* @param k*/public static void rotate2(int[] nums, int k) {// k = k%数组的长度,eg k=3%7=3;k %= nums.length;//反转所有数字reversal(nums, 0, nums.length - 1);//反转前 k 个数字reversal(nums, 0, k - 1);//反转后 n-k 个数字reversal(nums, k, nums.length - 1);System.out.println("方式二 使用反转:nums = " + JSON.toJSONString(nums));}/*** 反转数组** @param nums* @param start 数组开始下标* @param end 数组结束下标*/private static void reversal(int[] nums, int start, int end) {while (start < end) {int temp = nums[start];nums[start] = nums[end];nums[end] = temp;start++;end--;}}
运行代码
//方式2:nums = new int[]{1, 2, 3, 4, 5, 6, 7};ArrayAlgorithmToPractice.rotate2(nums, 3);
习题3方式2 复杂度分析
- 时间复杂度:O(n)。n为数组的元素。n 个元素被反转了总共 3 次。
- 空间复杂度:O(1)。常数位的空间复杂度,没有额外的空间被使用
习题4:存在重复元素
给定一个整数数组,判断是否存在重复元素。
如果任意一值在数组中出现至少两次,函数返回 true。如果数组中每个元素都不相同,则返回 false 。
示例 1:输入: [1,2,3,1] 输出: true
示例 2:输入: [1,2,3,4] 输出: false
示例3:输入: [1,1,1,3,3,4,3,2,4,2] 输出: true
习题4方式一: 使用set集合的方式
/*** TODO 存在重复元素 方式1:使用set集合的方式* 给定一个整数数组,判断是否存在重复元素。* 如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。** 作者:华星详谈* @param nums* @return*/public static boolean containsDuplicate(int[] nums) {Set<Integer> set = new HashSet();for(int i=0;i < nums.length;i++){if(!set.add(nums[i])){return true;}}return false;}
习题4方式一 复杂度分析
该方式多创建了一个Set集合空间,会有一些额外的开销。
- 时间复杂度: O(N),其中 N 为数组的长度;
- 空间复杂度:O(N),额外创建了set集合,n为set集合的长度。
习题4方式二:排序之后判断相邻元素的是否相等
/*** TODO 存在重复元素 方式2:排序之后判断相邻元素的是否相等* 给定一个整数数组,判断是否存在重复元素。* 如果任意一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。** 作者:华星详谈* @param nums* @return*/public static boolean containsDuplicate2(int[] nums) {Arrays.sort(nums);for(int i=1;i <nums.length;i++){if(nums[i-1] == nums[i]){return true;}}return false;}
习题4方式二 复杂度分析
该方法在对数组进行排序之后直接判断相邻两个元素是否相等,若相等则说明有相同的元素。
- 时间复杂度:O(N log N),其中 N 为数组的长度。需要对数组进行排序。
- 空间复杂度:O(log n)。
习题4方式二优化版
public static boolean containsDuplicate3(int[] nums) {return IntStream.of(nums).distinct().count() != nums.length;}
核心是使用Java8 的新特性之IntStream流。通过distinct()方法去重,count()汇总总数和数组的长度进行比较。其时间复杂度和空间复杂度与都为O(n)。
习题5:只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:输入: [2,2,1]输出: 1
示例 2:输入: [4,1,2,1,2]输出: 4
习题5方式一:使用hashSet集合方式
/*** TODO 只出现一次的数字 方式一:使用hashSet集合方式* 说明:* 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?* <p>* 作者:华星详谈* @param nums* @return*/public static int singleNumber(int[] nums) {Set<Integer> set = new HashSet<>();for (int num : nums) {if (!set.add(num)) {//添加不成功,则说明是重复的数据,则删除对应的元素set.remove(num);}}return set.isEmpty() ? -1 : set.iterator().next();}
习题5方式一 复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n²)
习题5方式二:使用Java8 新特性-分组取list长度为1
/*** TODO 只出现一次的数字 方式二:分组取list长度为1* 说明:* 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?* <p>* 作者:华星详谈* @param nums* @return*/public static int singleNumber2(int[] nums) {List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.groupingBy(value -> value)).entrySet().stream().filter(i -> i.getValue().size() == 1).map(Map.Entry::getKey).collect(Collectors.toList());return list.isEmpty() ? -1 : list.get(0);}
习题6.两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例 1: 输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2,2]
示例 2: 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[4,9]
说明:
输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
我们可以不考虑输出结果的顺序。
进阶:
如果给定的数组已经排好序呢?你将如何优化你的算法?
如果 nums1 的大小比 nums2 小很多,哪种方法更优?
如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
习题6方式一:使用Java8 新特性
* TODO 两个数组的交集 方式一:使用Java8新特性* 说明:* 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。* 我们可以不考虑输出结果的顺序。* <p>* 作者:华星详谈** @param nums1* @param nums2* @return*/public int[] intersect(int[] nums1, int[] nums2) {List<Integer> list1 = Arrays.stream(nums1).sorted().boxed().collect(Collectors.toList());List<Integer> list2 = Arrays.stream(nums2).sorted().boxed().collect(Collectors.toList());List<Integer> collect = list1.stream().filter(num -> list2.contains(num)).collect(Collectors.toList());return collect.stream().mapToInt(Integer::valueOf).toArray();}
习题6方式二:使用哈希表
* TODO 两个数组的交集 方式二:使用hash表的方式* 说明:* 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。* 我们可以不考虑输出结果的顺序。* <p>* 作者:华星详谈** @param nums1 长度较短的数组* @param nums2 长度较长的数组* @return*/public static int[] intersect(int[] nums1, int[] nums2) {//为了使算法最优化,在最外层迭代数组长度较长的那个if (nums1.length > nums2.length) {return intersect(nums2, nums1);}//int[] result = new int[nums2.length];Set<Integer> set = new HashSet();for (int num : nums2) {for (int i : nums1) {if (num == i) {set.add(i);}}}return set.stream().mapToInt(Integer::valueOf).toArray();}
在上面的代码中,我们使用了双重for循环,这样会增加代码的时间复杂度,我们可以对以上的代码再次进行优化。
习题6方式二:使用哈希表(优化版)
/*** TODO 两个数组的交集 方式二:使用hash表的方式(优化版)* 说明:* 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。* 我们可以不考虑输出结果的顺序。* <p>* 作者:华星详谈** @param nums1 长度较短的数组* @param nums2 长度较长的数组* @return*/public static int[] intersect3(int[] nums1, int[] nums2) {//为了使算法最优化,在最外层迭代数组长度较长的那个if (nums1.length > nums2.length) {return intersect3(nums2, nums1);}//定义一个map,存放每个元素出现的次数Map<Integer, Integer> map = new HashMap<>();for (int num : nums2) {int count = map.getOrDefault(num, 0) + 1;map.put(num, count);}int index = 0;int[] result = new int[nums1.length];for (int num : nums1) {int count = map.getOrDefault(num, 0);if (count > 0) {//说明该元素存在于num1、num2中result[index++] = num;//该元素已经放入result,自身减一count--;if (count > 0) {map.put(num, count);} else {map.remove(num);}}}//返回index长度的排序集合return Arrays.copyOfRange(result, 0, index);}
习题6方式三:使用排序后双指针的方式,(当num2长度过大,内存放不下是不适用) ps:执行时间耗时最少
解题思路:
1:先对数组进行排序
2:比较数组长度进行迭代,当两个索引都比长度小时,才进入循环
3:如果nums1[index1]、nums2[index2] 相等,则说明元素存在于num1和num2中,把该元素存放在新的数组中
4:如果nums1[index1]> nums2[index2],index2++;
5:如果nums1[index1]< nums2[index2],index1++;
/**
* TODO 两个数组的交集 方式三:使用排序后双指针的方式,(当num2长度过大,内存放不下是不适用) ps:执行时间耗时最少
* 说明:
* 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
* 我们可以不考虑输出结果的顺序。
* <p>
* 作者:华星详谈
*
* @param nums1 长度较短的数组
* @param nums2 长度较长的数组
* @return
*/
public static int[] intersect4(int[] nums1, int[] nums2) {
//为了使算法最优化,在最外层迭代数组长度较长的那个
if (nums1.length > nums2.length) {
return intersect4(nums2, nums1);
}
Arrays.sort(nums1);
Arrays.sort(nums2);
int length1 = nums1.length, length2 = nums2.length;
int index = 0, index1 = 0, index2 = 0;
int[] result = new int[Math.min(length1, length2)];
//当两个索引都比长度小时,才进入循环
while (index1 < length1 && index2 < length2) {
//元素较小的右移一位
if (nums1[index1] < nums2[index2]) {
index1++;
} else if (nums1[index1] > nums2[index2]) {
index2++;
} else {
result[index++] = nums1[index1];
index1++;
index2++;
}
}
return Arrays.copyOfRange(result, 0, index);
}
习题7、加一
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:输入:digits = [1,2,3] 输出:[1,2,4] 解释:输入数组表示数字 123。
示例 2:输入:digits = [4,3,2,1]输出:[4,3,2,2] 解释:输入数组表示数字 4321。
示例 3:输入:digits = [0]输出:[1]
提示:
- 1 <= digits.length <= 100
- 0 <= digits[i] <= 9
习题7方式一:
解题思路:
判断digits.length 是否为9 如果为9就改为1,继续循环digits.length-1,若为不等于9,则+1。最后处理特殊情况 99。
/**
* TODO 加一 解题思路:判断digits.length 是否为9 如果为9就改为1,继续循环digits.length-1,若为不等于9,则+1。最后处理特殊情况 99。
* 说明:
* 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
* 我们可以不考虑输出结果的顺序。
* <p>
* 作者:华星详谈
*
* @param digits
* @return
*/
public static int[] plusOne(int[] digits) {
//解题思路:判断digits.length 是否为9 如果为9就改为1,继续循环digits.length-1,若为不等于9,则+1。最后处理特殊情况 99。
for (int i = digits.length - 1; i >= 0; i--) {
if (digits[i] == 9) {
digits[i] = 0;
} else {
digits[i]++;
return digits;
}
}
//处理最高位为9的情况
int[] result = new int[digits.length + 1];
result[0] = 1;
return result;
}
习题7 方式二:digits[i]++ ,然后 digits[i] 取余 10,如果能够被整除,说明进一位,如果不能够被整除则直接返回
/**
* TODO 加一 解题思路:digits[i]++ ,然后 digits[i] 取余 10,如果能够被整除,说明进一位,如果不能够被整除则直接返回
* 说明:
* 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
* 我们可以不考虑输出结果的顺序。
* <p>
* 作者:华星详谈
*
* @param digits
* @return
*/
public static int[] plusOne2(int[] digits) {
//解题思路:digits[i]++ ,然后 digits[i] 取余 10,如果能够被整除,说明进一位,如果不能够被整除则直接返回
for (int i = digits.length - 1; i >= 0; i--) {
digits[i]++;
digits[i] %= 10;
if (digits[i] != 0) {
//说明不能够整除
return digits;
}
}
//处理特殊情况:最高位为9时
int[] result = new int[digits.length + 1];
result[0] = 1;
return result;
}
习题8 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例: 输入: [0,1,0,3,12] 输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
习题8 解题步骤
解题思路:
使用双指针的模式,定义 left 和 right 指针。
- 当 nums[left] 等于0时,移动left指针;
- 当 nums[left] 不等于0时,分两种情况
- left 指针大于right时,说明前一次循环中的元素为0,故进行元素替换。移动 left、right 指针。
- left 指针小于等于right时,直接移动 left、right 指针。
/**
* TODO 移动零:使用双指针
* 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
* 说明:
* 必须在原数组上操作,不能拷贝额外的数组。
* 尽量减少操作次数。
* <p>
* 作者:华星详谈
*
* @return
*/
public static void moveZeroes(int[] nums) {
int left = 0, right = 0;
while (left < nums.length) {
if (nums[left] != 0) {
if (left > right) {
nums[right] = nums[left];
nums[left] = 0;
}
right++;
}
left++;
}
System.out.println("习题8 移动零:使用双指针:" + JSONObject.toJSONString(nums));
}
习题9 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
你可以按任意顺序返回答案。
示例 1:输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:输入:nums = [3,3], target = 6 输出:[0,1]
习题9 方式一 使用数组的方式
解题思路
数组转list,判断target-当前元素的值在list中是否存在,若存在返回对应的下标,若不存在,进入下一个循环
/**
* TODO 两数之和:解题思路:数组转list,判断target-当前元素的值在list中是否存在,若存在返回对应的下标,若不存在,进入下一个循环
* 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
* 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
* 你可以按任意顺序返回答案。
* <p>
* 作者:华星详谈
*
* @return
*/
public static int[] twoSum(int[] nums, int target) {
//解题思路:数组转list,判断target-当前元素的值在list中是否存在,若存在返回对应的下标,若不存在,进入下一个循环
List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());
int[] result = new int[nums.length];
int index = 0;
for (int i = 0; i < nums.length; i++) {
int i1 = target - nums[i];
if (list.contains(i1)) {
if (list.indexOf(nums[i]) != list.lastIndexOf(i1)) {
result[0] = list.indexOf(nums[i]);
result[1] = list.lastIndexOf(i1);
index++;
break;
}
}
}
return Arrays.copyOfRange(result, 0, index + 1);
}
习题9 方式二:使用hash表的方式
解题思路:使用hash表的方式
- 迭代数组,如果target-num[i] 不存在map中存在的话,把元素和下标添加到map中;
- 如果target-num[i] 存在map中,返回对应的下标;
/**
* TODO 两数之和:方式二(推荐使用):解题思路:使用hash表的方式。定义一个map,存放元素和对应的下标,如果map中包含(target-当前元素),则返回他们的下标
* 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
* 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
* 你可以按任意顺序返回答案。
* <p>
* 作者:华星详谈
*
* @return
*/
public static int[] twoSum2(int[] nums, int target) {
//使用hash表的方式。定义一个map,存放元素和对应的下标,如果map中包含(target-当前元素),则返回他们的下标
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])) {
return new int[]{map.get(target - nums[i]), i};
}
map.put(nums[i], i);
}
return new int[0];
}
习题10 有效的数独
判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入:
[
["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: true
示例 2:
输入:
[
["8","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: false
解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。
但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
说明:
- 一个有效的数独(部分已被填充)不一定是可解的。
- 只需要根据以上规则,验证已经填入的数字是否有效即可。
- 给定数独序列只包含数字 1-9 和字符 ‘.’ 。
- 给定数独永远是 9x9 形式的。
解题思路
1:如何枚举子数独?
可以使用 box_index = (row / 3) * 3 + columns / 3,其中 / 是整数除法。
如何确保行 / 列 / 子数独中没有重复项?
可以利用 value -> count 哈希映射来跟踪所有已经遇到的值。
步骤:
- 遍历数独。
- 检查看到每个单元格值是否已经在当前的行 / 列 / 子数独中出现过:
- 如果出现重复,返回 false。
- 如果没有,则保留此值以进行进一步跟踪。
- 返回 true。
/**
* TODO 有效的数独
* 判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
*
* 数字 1-9 在每一行只能出现一次。
* 数字 1-9 在每一列只能出现一次。
* 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
* 数独部分空格内已填入了数字,空白格用 '.' 表示。
*
* 说明:
* 一个有效的数独(部分已被填充)不一定是可解的。
* 只需要根据以上规则,验证已经填入的数字是否有效即可。
* 给定数独序列只包含数字 1-9 和字符 '.' 。
* 给定数独永远是 9x9 形式的。
* <p>
* 作者:华星详谈
*
* @return
*/
public boolean isValidSudoku(char[][] board) {
int[][] hang = new int[9][9];//row
int[][] lie = new int[9][9];//colomn
int[][] gezi = new int[9][9];//9-digit cell
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
int x = board[i][j] - '0';
if (x >= 1 && x <= 9) {
hang[i][x - 1]++;
lie[j][x - 1]++;
gezi[i / 3 * 3 + j / 3][x - 1]++;
if (hang[i][x - 1] > 1 || lie[j][x - 1] > 1 || gezi[i / 3 * 3 + j / 3][x - 1] > 1) {
return false;
}
}
}
}
return true;
}
本文涉及到的文章
CSDN 博客专栏:https://blog.csdn.net/a767815662/category_10659228.html
文中代码 Git 地址: https://github.com/17666555910/KeyGenMe
