一. 数组的定义
在程序设计中,为了处理方便,常常需要把具有相同类型的若干元素按有序的形式组织起来,这种形式就是数组(Array)。数组是程序中最常见、也是最基本的数据结构。
对数组进行处理需要注意以下特点:
首先,数组会利用索引来记录每个元素在数组中的位置,且在大多数编程语言中,索引是从 0 算起的。我们可以根据数组中的索引,快速访问数组中的元素。事实上,这里的索引其实就是内存地址。
其次,作为线性表的实现方式之一,数组中的元素在内存中是连续存储的,且每个元素占用相同大小的内存。
以LeetCode上的数组相关题目为例,学习数组问题的算法。
二. 两数求和
力扣https://leetcode.cn/problems/two-sum/
1. 题目描述
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
你可以按任意顺序返回答案
示例:
输入: nums = [2, 7, 11, 15], target = 9
输出: [0, 1]
2. 解决方法
2.1 暴力法
穷举所有两数之和,双重for循环
public class TwoSum1 {
public int[] twoSum(int[] nums,int target){
int len = nums.length;
for(int i=0;i<len-1;i++){
for(int j=i+1;j<len;j++){
if(nums[i]+nums[j]==target){
return new int[]{i,j};
}
}
}
//如果找不到,抛出异常
throw new IllegalArgumentException("无结果");
}
public static void main(String[] args) {
int[] nums ={ 2, 7, 11, 15};
int target = 9;
TwoSum1 twoSum1 = new TwoSum1();
int[] res = twoSum1.twoSum(nums, target);
for (int re : res) {
System.out.print(re+"\t");
}
}
}
复杂度分析
时间复杂度:O(n^2)
空间复杂度:O(1)
2.2 哈希表
遍历数组,先将目标值与数组值相减(target-nums[i])后得到temp,判断hash表中是否存在temp,若满足条件则直接返回结果;不然将数据存入hash表,<k,v> = <元素值,索引值>。
public class TwoSum2 {
public int[] twoSum(int[] nums,int target){
int len = nums.length;
HashMap<Integer,Integer> hashMap = new HashMap<>();
for (int i = 0; i < len; i++) {
int temp = target-nums[i];
if(hashMap.containsKey(temp)){
return new int[]{i,hashMap.get(temp)};
}
hashMap.put(nums[i],i);
}
//如果找不到,抛出异常
throw new IllegalArgumentException("无结果");
}
public static void main(String[] args) {
int[] nums ={ 2, 7, 11, 15};
int target = 9;
TwoSum2 twoSum2 = new TwoSum2();
int[] res = twoSum2.twoSum(nums, target);
for (int re : res) {
System.out.print(re+"\t");
}
}
}
复杂度分析
时间复杂度:O(n),哈希表将查找时间缩短到 O(1)
空间复杂度:O(n),所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 N 个元素。
三. 两数求和
力扣https://leetcode.cn/problems/3sum/
1. 题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
示例:
输入: nums = [-1, 0, 1, 2, -1, -4]
输出: [[-1,-1,2],[-1,0,1]]
2. 解决方法
双指针法
双指针的思路,一般分为左右指针和快慢指针两种,以下使用左右指针。
1. 对数组进行排序
2. 遍历排序后的数组,进行判定:
(1)当 nums[i] > 0 时直接break跳出,因为 nums[R] >= nums[L] >= nums[i] > 0,即 3 个数字都大于 0 ,在此固定指针 i 之后不可能再找到结果了
(2) 当 i > 0且nums[i] == nums[i - 1]时,即遇到重复元素时,跳过此元素nums[i],因为已经将 nums[i - 1] 的所有组合加入到结果中,本次双指针搜索只会得到重复组合
3. 定义左右指针,左右指针位于此时数组索引两端,left = i+1, right = n-1
4. 当左右指针不重合时,计算nums[i],nums[left],nums[right]三数之和得sum,进行判定:
(1) 当sum=0时,将三数加入到结果集res中,且left++并跳过所有重复元素,right--并跳过所有重复元素
(2) 当sum>0时,right--,因为可能是num[right]过大导致
(3) 当sum<0时,left++,因为可能是num[left]过小导致
public class ThreeSum1 {
public List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
List<List<Integer>> res = new ArrayList();
Arrays.sort(nums);
for(int i=0;i<n;i++){
if(nums[i]>0)
break;
if(i>0 && nums[i]==nums[i-1])
continue;
int left = i+1, right = n-1;
while(left < right){
int sum = nums[i]+nums[left]+nums[right];
if(sum == 0){
res.add(Arrays.asList(nums[i],nums[left],nums[right]));
left++;
right--;
while(left<right && nums[left]==nums[left-1])
left++;
while(left<right && nums[right]==nums[right+1])
right--;
}else if(sum > 0){
right--;
}else{
left++;
}
}
}
return res;
}
public static void main(String[] args) {
int[] nums = {-1, 0, 1, 2, -1, -4};
ThreeSum1 threeSum1 = new ThreeSum1();
List<List<Integer>> res = threeSum1.threeSum(nums);
System.out.println(res);
}
}
复杂度分析:
时间复杂度: O(N^2),其中固定指针i循环复杂度 O(N),双指针 left,right复杂度 O(N)
空间复杂度: O(1),指针使用常数大小的额外空间
四. 下一个排列
力扣https://leetcode.cn/problems/next-permutation/
1. 题目描述
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
必须原地修改,只允许使用额外常数空间
示例:
输入: 1,3,8,7,6,2
输出: 1,6,2,3,7,8
2. 解决方法
可以观察到,若数组为全逆序,则其下一个排列为全升序。故当数组存在一个“升序子序列”时,必然可以找到它的下一个排列,所以思路如下:
1. 寻找最后一对升序子序列
2. 如果没找到,则说明数组全逆序,直接返回全升序
3. 如果找到了,将升序元素(以k为下标)与其后仅小于它的元素进行交换
4. 元素交换后,将k下标后的剩余所有元素反转为升序
public class NextPermutation {
public void nextPermutation(int[] nums) {
//找到右边第一对升序
int k = nums.length - 2;
while (k >= 0 && nums[k] >= nums[k + 1])
k--;
//如果全逆序,直接返回全升序
if (k < 0) {
Arrays.sort(nums);
return;
}
//找到k之后仅大于k位置的数并交换
int i = k + 2;
while (i < nums.length && nums[i] > nums[k])
i++;
int temp = nums[k];
nums[k] = nums[i - 1];
nums[i - 1] = temp;
//k以后剩余部分反转升序
int start = k+1,end =nums.length-1;
while (start < end){
int reTemp = nums[start];
nums[start] = nums [end];
nums[end] = reTemp;
start++;
end--;
}
}
public static void main(String[] args) {
int[] nums = {1,3,8,7,6,2};
NextPermutation nextPermutation = new NextPermutation();
nextPermutation.nextPermutation(nums);
for (int num : nums) {
System.out.print(num+"\t");
}
}
}
复杂度分析:
时间复杂度:O(N),其中 N 为给定序列的长度。我们至多只需要扫描两次序列,以及进行一次反转操作。
空间复杂度:O(1),只需要常数的空间存放若干变量。
五. 旋转图像 力扣
https://leetcode.cn/problems/rotate-image/
1. 题目描述
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例:
输入: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
2. 解决方法
2.1 数学方法(先转置再翻转)
利用矩阵的特性。所谓顺时针旋转,其实就是先转置矩阵,然后翻转每一行。
public class RotateImage1 {
public void rotate(int[][] matrix) {
int n = matrix.length;
//转置矩阵
for (int i = 0; i < n; i++)
for (int j = i; j < n; j++) {
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
//翻转行
for( int i = 0; i < n; i++ ){
for( int j = 0; j < n/2; j++ ){
int tmp = matrix[i][j];
matrix[i][j] = matrix[i][n-j-1];
matrix[i][n-j-1] = tmp;
}
}
}
public static void main(String[] args) {
int[][] matrix = {{5,1,9,11},{2,4,8,10},{13,3,6,7},{15,14,12,16}};
for(int i=0;i<matrix.length;i++){
for(int j=0;j<matrix[0].length;j++){
System.out.print(matrix[i][j]+"\t");
}
System.out.println();
}
System.out.println("=========转换后========");
RotateImage1 rotateImage1 = new RotateImage1();
rotateImage1.rotate(matrix);
for(int i=0;i<matrix.length;i++){
for(int j=0;j<matrix[0].length;j++){
System.out.print(matrix[i][j]+"\t");
}
System.out.println();
}
}
}
复杂度分析:
时间复杂度:O(N^2),这个简单的方法已经能达到最优的时间复杂度O(N^2) ,因为既然是旋转,那么每个点都应该遍历到,N^2的复杂度不可避免。
空间复杂度:O(1),旋转操作是原地完成的,只耗费常数空间。
2.2 分治法
旋转的时候,是上下、左右分别对称的,所以我们遍历元素的时候,只要遍历一半行、一半列就可以了(1/4元素),然后直接操作矩阵元素(从外圈向内依次旋转,直到中心点或者最内圈)进行交换。
public class RotateImage2 {
public void rotate(int[][] matrix) {
int n = matrix.length;
for(int i=0;i<(n+1)/2;i++){
for(int j=0;j<n/2;j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[n-j-1][i];
matrix[n-j-1][i] = matrix[n-j-1][n-i-1];
matrix[n-j-1][n-i-1] = matrix[j][n-i-1];
matrix[j][n-i-1] = temp;
}
}
}
public static void main(String[] args) {
int[][] matrix = {{5,1,9,11},{2,4,8,10},{13,3,6,7},{15,14,12,16}};
for(int i=0;i<matrix.length;i++){
for(int j=0;j<matrix[0].length;j++){
System.out.print(matrix[i][j]+"\t");
}
System.out.println();
}
System.out.println("=========转换后========");
RotateImage2 rotateImage2 = new RotateImage2();
rotateImage2.rotate(matrix);
for(int i=0;i<matrix.length;i++){
for(int j=0;j<matrix[0].length;j++){
System.out.print(matrix[i][j]+"\t");
}
System.out.println();
}
}
}
复杂度分析
时间复杂度:O(N^2),是两重循环的复杂度。
空间复杂度:O(1),我们在一次循环中的操作是“就地”完成的。