前文回顾
之前我们使用三天的时间,学习了整数章节的知识学习。并在Day4的总结中,结合读者们关于算法学习和刷题过程中的疑问,进行了相关解答。
当然朋友们也提了一些关于文章整体讲解上的建议,总体来说:
- 每章的第一篇文章会将本章知识点全部讲解,但是当天的题目并不涉及这些内容,等后面的题目用到该知识点的时候,还要返回去看本章的第一篇文章,比较麻烦。
- 算法题目讲解的时候,当前多为文字描述,希望能多添加一些配图帮助理解。
- 当前剑指offer学习群有些冷清,能组织一些刷题活动,类似交作业的方式会更好点。
谢谢大家提出的建议,也期待大家的持续反馈。
对于第一条,考虑之后将每章的知识点进行拆分,每天根据题目先讲解针对性的知识点后,再进行题目讲解。
第二条关于配图的这个,算是一条不足了,之后会配上图片或者动图,更为细致的讲解题目
最后一条嘛,就不能太强求了,毕竟很多朋友周内比较忙,可能更多的是看看题解,周末的时候再集中刷题。当然为了满足大家,之后考虑每天会留一道题目当做课后作业,然后第二天在发布当天内容的同时,讲解前一天的作业题目。内容可能是当前章节,也可能是之前某章题目的回顾。这道题目的讲解,放在文末或者单独开一篇文章。
浅析数据结构
OK,今天开始,我们要走进算法的第一个数据结构-----数组了。但在学习之前,我们首先应该对数据结构进行一个了解。
何为数据结构: 数据结构是计算机存储、组织数据的方式。
数据结构大致可划分为三类:线性结构、树形结构、图形结构。(画个丑图,大家理解下):
其中他们各自,又细化出了更多子结构,比如:
线性结构*(线性表)
- 数组
- 链表
- 栈
- 队列
- 哈希表(散列表)
- ps: 哈希表是一种特殊的线性表,采用了哈希算法。同时有链表和线性表的优点,但占的空间大,牺牲空间换取了效率。
树形结构
- 二叉树(完全二叉树、满二叉树、平衡二叉树)
- 堆
- Trie(字典树)
- B树
- 红黑树
- 哈夫曼树
- 并查集
图形结构
- 邻接矩阵
- 临街表
理解了这几种数据结构的分类,就可以开始本章的正式内容了。(话说前戏有点太多了....)
数组(Array)
根据上面的分类,我们了解到数组属于线性表的一种特殊场景。线性表是具有n个相同类型元素的序列。而数组则是一种顺序存储的线性表,其中所有元素的内存地址是连续的。
而在很多编程语言中,数组都有一个致命的缺点,一旦初始化后,则无法动态的去修改容量。
那么很多Python的小伙伴就说了,python的List和上面说的内容大相径庭啊!list不仅可以存储不同数据类型的元素,还支持动态扩容,完全不是一码事儿!
所以我专门把很多两个字加粗了。Python中list是采用分离式技术实现的动态顺序表。
- “分离式技术”,指的是表头信息和数据地址是分开的。
- "动态"指的是元素存储区可以更换,用以实现元素的增删。
在算法学习的初期,小伙伴们可以使用Python来刷题,因为Python的很多内置库与底层优化,可以方便我们写出更为简便的代码。但掌握了一定基础后,还是建议大家挑选一门更为底层的语言Java或者C++来进行二刷,以便了解更多Python中被开发者优化过的内容。至于C就算了,已经底层的有点入矿井了...
虽然Python有无脑list,但在这里还是希望Python的朋友们,也能继续往下来看看Java关于数组的一些知识,考虑下如果Python的List和Java的Array一样,我们该如何做题.
数组的初始化
在Python中,list由于为动态数组,所以初始化可以很随意:
- li = []
- li = list()
- li = [1, 2, 3, 4, 5]
但在Java中,由于数组初始化后,长度就无法再修改了,所以分为了静态初始化和动态初始化两种:
- 静态初始化: int[] arr = {1, 2, 3, 4, 5};
- 动态初始化: int[] arr1 = new int[5]; //数组通过int初始化后,每个元素默认为0。
数组的遍历
Python和Java在数据的遍历,都可以分为两种:
- 通过获取数组长度,然后控制下标变更来访问数组的元素
- 直接遍历每个元素
下标访问
python:
li = [1, 2, 3, 4, 5] for i in range(len(li)): print(li[i])
Java:
int[] arr = {1, 2, 3, 4, 5}; for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); }
遍历每个元素
Python:
for i in li: print(i) # 特殊的 enumerate操作,方便同时获取下标与内容 for index, val in enumerate(li, start=0): print(f"index:{index},value:{val}")
Java (for each):
int[] arr = {1, 2, 3, 4, 5}; for (int i:arr) { System.out.println(i); }
数组操作
打印数组及数组内存地址
Python:
li = [1, 2, 3, 4, 5] # 打印数组内存地址 print(id(li)) # 2020779057800 # 打印数组 print(li) # [1, 2, 3, 4, 5]
Java:
import java.util.Arrays; int[] arr = {1, 3, 2, 4, 5}; // 打印内存地址 System.out.println(arr); // [I@1b6d3586 // 打印数组 System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5] Arrays.sort(arr);
数组排序
Python:
# li.sort()原地修改 li = [1, 3, 2, 4, 5] print(li) # [1, 3, 2, 4, 5] li.sort() print(li) # [1, 2, 3, 4, 5] # sorted(li)创建新数组并返回 li = [1, 3, 2, 4, 5] new_li = sorted(li) print(new_li) # [1, 2, 3, 4, 5]
Java:
import java.util.Arrays; int[] arr = {1, 3, 2, 4, 5}; System.out.println(Arrays.toString(arr)); // [1, 3, 2, 4, 5] Arrays.sort(arr); System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]
今天作为数组的第一篇文章,先讲解如上内容,就可以满足下面两道题目的解题要求了。
双指针操作
数组是我们讲解的第一种数据结构,而下来要讲解的双指针,则是我们要学习的第一种解题思路。
这里说到的指针,不同于C的指针,而是一个通过初始化指针变量,对应数组不同index下标,从而在计算工程中移动指针搜索目标的解题思路。
不光是数组,字符串、链表等等也会用到指针。所以这次讲过后,等下次遇到就一带而过了。而指针的解题思路一般分为三类:
- 首尾指针:范围查找,比如二分搜索等
- 滑动窗口:指针处在数组同一方向,根据条件移动左右指针,用于获取范围和等
- 快慢指针: 多用于链表计算时,判断是否有环等
先来一道题热热身吧!
剑指OfferII006.排序数组中两个数字之和
https://leetcode-cn.com/problems/kLl5u1/solution/shua-chuan-jian-zhi-offer-day05-shu-zu-i-ygiw/
难度:简单
题目
给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,
所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length 。
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
提示:
- 2 <= numbers.length <= 3 * 10 ^ 4
- -1000 <= numbers[i] <= 1000
- numbers 按 递增顺序 排列
- -1000 <= target <= 1000
- 仅存在一个有效答案
示例
示例 1: 输入:numbers = [1,2,4,6,10], target = 8 输出:[1,3] 解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。 示例 2: 输入:numbers = [2,3,4], target = 6 输出:[0,2] 示例 3: 输入:numbers = [-1,0], target = -1 输出:[0,1]
分析
这可能是力扣有史以来最简单的一道题了,为什么?因为力扣第一题两数之和就跟这道题神似。
但是注意,两数之和是无序的,而这道题是有序的,所以是不是更简单呢?
当然两数之和可以使用的暴力双层循环和Hash表,这道题也一样可以。
但双层for循环是O(n ^ 2)的复杂度,而Hash表虽然时间复杂度是O(n),但空间复杂度一样是O(n).
然而,对于有序数组的两数之和,我们可以使用刚才提到的首尾双指针的解题思路来完成该题目。
- 首先设置left、right指针分别指向数组的首、尾
- 判断number[left] + number[right] 的和total与target的大小
- 如果total > target,right 右移
- 如果total < target,left 左移
- 由于结果必然存在,最终返回left和right即可
时间复杂度O(n),空间复杂度O(1)
解题
Python:
class Solution: def twoSum(self, numbers, target): left, right = 0, len(numbers) - 1 while left < right: if numbers[left] + numbers[right] == target: return [left, right] elif numbers[left] + numbers[right] > target: right -= 1 else: left += 1
Java:
class Solution { public int[] twoSum(int[] numbers, int target) { int left = 0; int right = numbers.length - 1; while (left < right) { int total = numbers[left] + numbers[right]; if (total == target) { return new int[]{left, right}; } else if (total > target) { right--; } else { left++; } } return new int[2]; } }
这道无脑入门题目,由于过于简单,就不细致讨论了,下来看一道进阶版本的题目。只要上面的题目理解了,这道题注意下细节,即可轻松解题!
剑指OfferII007.数组中和为0的三个数
https://leetcode-cn.com/problems/1fGaJU/solution/shua-chuan-jian-zhi-offer-day05-shu-zu-i-e2af/
难度:中等
题目
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c ,使得 a + b + c = 0 ?
请找出所有和为 0 且 不重复 的三元组。
提示:
- 0 <= nums.length <= 3000
- -10 ^ 5 <= nums[i] <= 10 ^ 5
示例
示例 1: 输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 示例 2: 输入:nums = [] 输出:[] 示例 3: 输入:nums = [0] 输出:[]
分析
这道题算是上一道题的升华版,那么三层for循环是不是可以走一波?按照这个数量级有点风险,但这种暴力解法就不展开了。
我们打算接着使用刚才双指针的思想,但是三个数该如何操作?无非是在双指针的外面套一层for循环而已。
这道题的数组是没有排序的,所以在前面单独介绍了Python和Java数组的排序操作。
另外需要注意的是,题目要求不重复的三元组,为了保持不重复,我们可以:
- 使用集合进行去重
- 指针偏移时,判断下一个val是否等于当前val,若等于则跳过的操作来实现
理解了这三点,有前一道题目解析的铺垫,就可以大胆操作了!
最后,还有一个优化点,如果第一个数字已经大于0了,那直接可以break了...
解题
Python:
class Solution: def threeSum(self, nums): nums.sort() lg, ret = len(nums), [] for i in range(lg - 2): if nums[i] > 0: break if i > 0 and nums[i] == nums[i - 1]: continue left = i + 1 right = lg - 1 while left < right: total = nums[i] + nums[left] + nums[right] if total == 0: ret.append([nums[i], nums[left], nums[right]]) val = nums[left] while left < right and nums[left] == val: left += 1 elif total > 0: right -= 1 else: left += 1 return ret
Java:
class Solution { public static List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> ans = new ArrayList(); int lg = nums.length; Arrays.sort(nums); for (int i = 0; i < lg; i++) { if (nums[i] > 0) break; if (i > 0 && nums[i] == nums[i - 1]) continue; int left = i + 1; int right = lg - 1; while (left < right) { int total = nums[i] + nums[left] + nums[right]; if (total == 0) { ans.add(Arrays.asList(nums[i], nums[left], nums[right])); while (left < right && nums[left] == nums[left + 1]) left++; left++; } else if (total < 0) left++; else if (total > 0) right--; } } return ans; } }
课后习题
既然咱们已经做了两数之和、三数之和,那么留一个简单的作业吧:
力扣 18.四数值和 走起来吧。