概述
算法是一个程序员的核心竞争力,也是面试最重要的考查环节。本文整理一些与数组相关的基础算法题。
涉及解题算法(思想):动态规划、双指针(在链表求环等考题中经常出现)、
注:题目均搜集自网络,仅做汇总整理备忘录用。
考题
合并两个有序数组
LeetCode链接。
public static void merge(int[] nums1, int m, int[] nums2, int n) {
int len = m + n;
int i = m - 1;
int j = n - 1;
// 从后向前归并,比较nums1和nums2末尾的元素哪个大,谁大谁出列,覆盖nums1
for (int k = len - 1; k >= 0; k--) {
if (i == -1) {
// 数组下标越界异常
// 此时j位于数组nums2的末尾索引位置,还未看的数组nums2的长度为j + 1
System.arraycopy(nums2, 0, nums1, 0, j + 1);
break;
} else if (j == -1) {
break;
} else if (nums1[i] >= nums2[j]) {
// 谁大谁出列
nums1[k] = nums1[i];
i--;
} else {
assert nums1[i] < nums2[j];
nums1[k] = nums2[j];
j--;
}
}
}
多数元素
给定一个大小为n的数组,找到其中的多数元素和出现次数。多数元素:在数组中出现次数大于 ⌊n/2⌋
的元素。
假定:数组非空且总是存在多数元素。
要求:时间复杂度为O(n)、空间复杂度为O(1)
public static int getMajority(int[] nums) {
int count = 0;
int result = 0;
for (int item : nums) {
if (item == result) {
count += 1;
} else {
if (count == 0) {
result = item;
count = 1;
} else {
count -= 1;
}
}
}
return result;
}
两数之和
从给定的数组nums里面找到满足条件的两个数,使其和为目标值target。返回满足条件的一组结果即可,返回索引值。
分析:结果存在多种情况,如果题目要求只返回一种:
public static int[] twoSum(int[] nums, int target) {
int[] answer = new int[2];
int n = nums.length;
for (int i = 0; i < n - 1; i++) {
int t = target - nums[i];
answer[0] = i + 1;
for (int j = i + 1; j < n; j++) {
if (nums[j] > t) {
break;
}
if (nums[j] == t) {
answer[1] = j + 1;
break;
}
}
if (answer[1] != 0) {
break;
}
}
return answer;
}
除了两层for循环外,还可以使用哈希来解决此问题:
public static int[] twoSum(int[] nums, int target) {
// 存储数组元素和索引的映射关系
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int res = target - nums[i];
if (map.containsKey(res)) {
return new int[]{map.get(res), i};
}
map.put(num[i], i);
}
return new int[]{};
}
有序数组
如果给定的数组是有序(升序)的,则可以考虑使用双指针算法。
使用两个指针分别指向数组的第一个元素和最后一个元素,如果两个指针指向的两个元素和小于给定值,则前指针后移,增大两元素和;相反的,如果大于给定值则后指针前移,减小两元素和。如果等于,则找到一组解,如果直至两指针相遇还没找到,则无解。
public static int[] twoSum(int[] a, int target) {
int i = 0, j = a.length - 1;
while (true) {
if (target == a[i] + a[j]) {
return new int[]{i, j};
}
if (a[i] + a[j] < target) {
++i;
} else {
--j;
}
if (i == j) {
return null;
}
}
}
三数之和为0
给定一个包含n个整数的数组nums,判断nums中是否存在三个元素 a,b,c,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
两数之和的升级版。
可以考虑使用双指针法:遍历数组,以当前元素的相反数为两数和,然后在当前元素后的所有元素范围内使用双指针算法寻找另两个元素。
public static List<List<Integer>> threeSum(int[] num) {
Arrays.sort(num);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < num.length - 2; ++i) {
if (i > 0 && num[i] == num[i - 1]) {
continue;
}
int lo = i + 1, hi = num.length - 1, sum = -num[i];
while (lo < hi) {
// 有序数组找特定和的两元素,双指针算法
if (num[lo] + num[hi] == sum) {
res.add(Arrays.asList(num[i], num[lo], num[hi]));
while (lo < hi && num[lo] == num[lo + 1]) {
lo++;
}
while (lo < hi && num[hi] == num[hi - 1]) {
hi--;
}
lo++;
hi--;
} else if (num[lo] + num[hi] < sum) {
lo++;
} else {
hi--;
}
}
}
return res;
}
如果需要返回全部满足条件的组合呢?
组合总和
给定一个无重复元素的数组nums和目标数target,找出nums中所有可以使数字和为target的组合。
注:nums中的数字可以无限制重复被选取,组合不能重复。
public static List<List<Integer>> combinationSum(int[] nums, int target) {
List<List<Integer>> resultList = new ArrayList<>();
List<Integer> result = new ArrayList<>();
Arrays.sort(nums);
dfs(nums, resultList, result, 0, target);
return resultList;
}
private static void dfs(int[] nums, List<List<Integer>> resultList, List<Integer> result, int start, int target) {
if (target >= 0) {
if (target == 0) {
resultList.add(new ArrayList<>(result));
} else {
for (int i = start; i < nums.length; i++) {
result.add(nums[i]);
dfs(nums, resultList, result, i, target - nums[i]);
result.remove(result.size() - 1);
}
}
}
}
查找两数
给定一个数组 n u m s nums nums,和一个整数 k k k,判断数组中是否存在两个不同的索引 i i i和 j j j,使得 n u m s [ i ] = n u m s [ j ] nums [i]=nums[j] nums[i]=nums[j],且 i i i和 j j j的差的绝对值至多为 k k k。
public static boolean containsNearbyDuplicate(int[] nums, int k) {
int left = 0;
int right = -1;
HashMap<Integer, Integer> hashMap = new HashMap<>();
while (left < nums.length) {
if (right + 1 < nums.length && right - left < k) {
right++;
if (hashMap.containsKey(nums[right])) {
return true;
} else {
hashMap.put(nums[right], 1);
}
} else {
hashMap.put(nums[left], hashMap.get(nums[left]) - 1);
if (hashMap.get(nums[left]) == 0) {
hashMap.remove(nums[left]);
}
left++;
}
}
return false;
}
股票交易的最大利润
给定一个非负整数元素组成的数组,其中的元素值表示股票的每日价格,求能获得的股票交易最大利润。
注:股票交易必须先买入,后卖出;可以多次买入卖出;只有卖出后才能获取到利润。
示例:
输入:[3, 5, 4, 3, 1, 4]
输出:5
public static int maxProfit(int[] prices) {
int temp = 0;
// 终止条件为length-1,防止数组越界
for (int i = 0; i < prices.length - 1; i++) {
if (prices[i + 1] > prices[i]) {
temp = temp + prices[i + 1] - prices[i];
}
}
return temp;
}
上面这个股票交易更贴近现实,即可以多次买入卖出。
变种
如果限定股票的买入和卖出都只能操作一次,求最大利润,该如何实现呢。
示例输入:[7,1,5,3,6,4]
,输出应该是6-1=5
。
最简单的方法就是双重循环,暴力破解,源码如下:
public static int maxProfit2(int[] prices) {
int max = 0;
for (int i = 0; i < prices.length - 1; i++) {
for (int j = i + 1; j < prices.length; j++) {
if ((prices[j] - prices[i]) > max) {
max = prices[j] - prices[i];
}
}
}
return max;
}
双重for循环,效率太低,在给定的执行时间条件限制下,有些测试用例无法通过。
因此需要寻找更有的解决方法,一次for循环求得最大收益,需要额外定义一个变量记录股票的最低价,并且动态更新到目前为止已知的最高收益。源码如下:
public static int maxProfit3(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int maxProfit = 0;
for (int price : prices) {
if (price < minPrice) {
// 记录最低价
minPrice = price;
} else {
// 动态更新最高收益
maxProfit = Math.max(maxProfit, price - minPrice);
}
}
return maxProfit;
}
连续元素的最大和
给定一个包含负数的整数数组,求连续元素的最大求和。
public static int maxSubArray(int[] nums) {
int maxSum = nums[0];
int curSum = 0;
for (int n : nums) {
curSum += n;
if (curSum > maxSum) {
maxSum = curSum;
}
if (curSum < 0) {
curSum = 0;
}
}
return maxSum;
}
动态规划:
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
给定一个问题,把它拆成一个个子问题,直到子问题可以直接解决。把子问题答案保存起来,以减少重复计算。最后根据子问题答案反推,得出原问题解的一种方法。
最核心思想,拆分子问题,记住过往,减少重复计算。
public static int maxSubArray1(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
max = Math.max(max, dp[i]);
}
return max;
}
未出现的最小正整数
给你一个未排序的整数数组 nums ,找出其中未出现的最小正整数。
public static int firstMissingPositive(int[] nums) {
int ls = nums.length;
int index = 0;
while (index < ls) {
// 正数; while循环结束条件;
if (nums[index] <= 0 || nums[index] > ls || nums[nums[index] - 1] == nums[index]) {
index += 1;
} else {
int pos = nums[index] - 1;
int tmp = nums[index];
nums[index] = nums[pos];
nums[pos] = tmp;
}
}
int res = 0;
return res + 1;
}
排序数组求某元素的索引位置
给定一个升序排列的整数数组nums,目标值target。找出给定目标值在数组中的开始位置和结束位置;如果数组中不存在目标值 target,返回[-1, -1]
。
注:元素可能不存在,也可能出现不止2次。
public static int[] searchRange(int[] nums, int target) {
int[] result = new int[2];
result[0] = floor(nums, target);
result[1] = ceil(nums, target);
return result;
}
private static int floor(int[] nums, int target) {
int left = -1;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
if (target <= nums[mid]) {
right = mid - 1;
} else {
left = mid;
}
}
if (left + 1 < nums.length && nums[left + 1] == target) {
return left + 1;
} else {
return -1;
}
}
private static int ceil(int[] nums, int target) {
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (target >= nums[mid]) {
left = mid + 1;
} else {
right = mid;
}
}
if (right - 1 >= 0 && nums[right - 1] == target) {
return right - 1;
} else {
return -1;
}
}
不重复的元素
给定一个包含若干重复元素的数组,找出第一个未重复的元素。
分析:存在重复2次或以上的情况;存在多个元素未重复的情况。
public static int singleNumber(int[] nums) {
Arrays.sort(nums);
int res = 0;
int i = 0;
for (int j = 1; j < nums.length; j++) {
if (nums[j] != nums[i]) {
if (j - i == 1) {
res = nums[i];
break;
} else {
i = j;
}
}
}
if (i == nums.length - 1) {
res = nums[i];
}
return res;
}
搜索并插入有序数组
给定一个有序数组,和目标值target,将target插入到数组中,返回其索引值。
分析:数组可能包括负数,可能存在重复值,目标值也可能与数组元素重复。
public static int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
if (target < nums[left]) {
return 0;
}
if (target > nums[right]) {
return nums.length;
}
while (left <= right) {
int mid = (right - left) / 2 + left;
if (target < nums[mid]) {
right = mid - 1;
} else if (target > nums[mid]) {
left = mid + 1;
} else {
return mid;
}
}
return left;
}
数组元素可组成的最大数
给定一组非负整数nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。注:输出结果可能非常大,返回一个字符串。
示例:
输入:[3, 30, 34, 5, 9]
输出:9534330
分析:题目要求返回字符串,那可以使用字符串的对比API,即compareTo方法,
public static String largest(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
String tmp1 = String.valueOf(nums[j]);
String tmp2 = String.valueOf(nums[j + 1]);
if ((tmp1 + tmp2).compareTo(tmp2 + tmp1) < 0) {
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
StringBuilder re = new StringBuilder();
for (int item : nums) {
re.append(item);
}
return re.toString();
}
跳跃游戏
给定一个非负整数数组,最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。
目标:使用最少的跳跃次数到达数组的最后一个位置。
public static int jump(int[] nums) {
int end = 0;
int steps = 0;
int maxPosition = 0;
// 不能等于length-1
for (int i = 0; i < nums.length - 1; i++) {
maxPosition = Math.max(maxPosition, i + nums[i]);
if (i == end) {
end = maxPosition;
steps++;
}
}
return steps;
}
移除元素
给定一个数组nums和目标值target,原地移除所有数值等于target的元素,返回移除后数组的新长度。
注:不能使用额外的数组空间,必须仅使用 O ( 1 ) O(1) O(1)额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
public static int removeElement(int[] nums, int target) {
int len = nums.length;
for (int i = 0; i < len; ) {
if (nums[i] == target) {
nums[i] = nums[len - 1];
len--;
} else {
i++;
}
}
log.info(Arrays.toString(Arrays.stream(nums).limit(len).toArray()));
return len;
}
盛水最多
给定一个数组,数组每个元素值表示其高度值,单位值为1,每两个元素间隔单位值为1。为简化问题,假设水的宽度为单位1。求能盛水的最大体积。
示例:
输入:[1, 8, 6, 2, 5, 4, 8, 3, 7]
输出:49
分析:最简单的方法就是暴力破解,两层for循环,直接给出源码:
public static int maxArea1(int[] height) {
int max = 0;
for (int i = 0; i < height.length; i++) {
for (int j = 1; j < height.length; j++) {
int h = Math.min(height[j], height[i]);
if ((j - i) * h > max) {
max = (j - i) * h;
}
}
}
return max;
}
时间复杂度太高。测试用例失败,需要优化,能不能只通过一层for循环解决问题?
双指针思想:
public static int maxArea(int[] height) {
int res = 0;
int temp;
for (int i = 0, j = height.length - 1; i < j; ) {
temp = Math.min(height[i], height[j]) * (j - i);
res = Math.max(res, temp);
if (height[i] > height[j]) {
j--;
} else {
i++;
}
}
return res;
}
盛水
给定一个数组,其数值表示高度,求最大能盛水单位,以及起始和终止元素值。
public static int[] maxArea(int[] height) {
int N = height.length;
int i = 0;
int j = N - 1;
int max = 0;
int[] result = new int[5];
while (i < j) {
int c = (j - i) * Math.min(height[i], height[j]);
if (c > max) {
max = c;
result[0] = c;
result[1] = height[i];
result[2] = height[j];
result[3] = i;
result[4] = j;
}
if (height[i] > height[j]) {
j--;
} else {
i++;
}
}
return result;
}
矩阵生成
给定一个正整数 n n n,生成一个包含 1 − n 2 1-n^2 1−n2全部元素的矩阵,且各元素按顺时针递增。
public static int[][] generateMatrix(int n) {
int[][] res = new int[n][n];
if (n == 0) {
return res;
}
int left = 0;
int right = n - 1;
int up = 0;
int down = n - 1;
int i = 1;
while (i <= n * n) {
for (int col = left; col <= right; col++) {
res[up][col] = i;
i++;
}
up++;
if (i <= n * n) {
for (int j = up; j <= down; j++) {
res[j][right] = i;
i++;
}
right--;
}
if (i <= n * n) {
for (int j = right; j >= left; j--) {
res[down][j] = i;
i++;
}
down--;
}
if (i <= n * n) {
for (int j = down; j >= up; j--) {
res[j][left] = i;
i++;
}
left++;
}
}
return res;
}