数组是一种基础数据结构,可以用来处理常见的排序和二分搜索问题,典型的处理技巧包括对撞指针、滑动窗口等,数组是数据结构中的基本模块之一。因为字符串是由字符数组形成的,所以二者是相似的。大多数面试问题都属于这个范畴。
数组之中常和双指针、二分查找、排序算法、字符串、矩阵相结合,以及分治算法
剑指offer 数组(共14道题目):
HashSet
特点)【剑指 Offer】12、矩阵中的路径
【剑指 Offer】66、构建乘积数组
27、移除元素
26、删除有序数组中的重复项
80、删除有序数组中的重复项 II
75、颜色分类(双指针)
字符串的带参构造函数,实现传入char[]
//String(char[] ch)
new String(ch);
HashSet的带参构造函数
//HashSet(Collection extends E> c) 构造一个包含指定 collection 中的元素的新 set。
new HashSet<>(Arrays.asList('a','e','i','o','u','A','E','I','O','U'));
//Arrays.asList(T... a) 返回一个受指定数组支持的固定大小的 List
Character数组的操作
if (Character.isLetterOrDigit(ch)) {
list.append(Character.toLowerCase(ch));
}
在字符数组中++操作的灵活运用
nums[i++] = a;//将nums[i] = a,并且i++
防止覆盖的数组后移操作
//从前往后挪的操作会导致数据的覆盖
for(int i = left;i<nums.length;i++){
nums[i+1] = nums[i];//nums[1,2,2,3,0,0]——>{1,2,2,2,2,2}
}
int last = m+n-1;
while(j>=0){
nums1[last--] = nums2[j--];
}
Arrays.copyOf()
,实现将集合转化成数组
//copyOf(int[] original, int newLength),复制指定的数组,截取或用 0 填充(如有必要),以使副本具有指定的长度
//copyOfRange(int[] original, int from, int to) ,将指定数组的指定范围复制到一个新数组。
//返回新的数组
Arrays.copyOfRange(res, 0, index+1);
ArrayList.toArray()
实现将对应的集合list转化成相同数据类型大小的数组
//list中已经存储了元素
Object[] obj = list.toArray();
//不带参数的toArray()方法,是构造的一个Object数组,然后进行数据copy
Integer[] integers = list.toArray(new Integer[list.size()]);
int[][] ans = list.toArray(new int[list.size()][]);//返回转化之后的int[][]二维数组
//带参数的toArray(T[] a) 方法,根据参数数组的类型,构造了一个对应类型的,长度跟ArrayList的size一致的数组
//源码
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
Lambda
表达式Arrays.sort
排序操作
Arrays.sort(arrays, (a, b) -> a[0] - b[0]);//谁小谁在前
//相当于
Arrays.sort(arrays, new Comparator<int[]>() {
@Override
public int compare(int[] a, int[] b) {
return a[0]-b[0];
}
});
27、移除元素 (快慢指针)
26、删除有序数组中的重复项 (快慢指针)
80、删除有序数组中的重复项 II
75、颜色分类(双指针,三色旗,小米笔试)
// 快慢指针移除元素
/**
* 27.移除元素
* 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于
* val 的元素,并返回移除后数组的新长度。
* @param arr
*/
public int remove(int[] arr,int val){
int slow = 0;
for (int fast = 0; fast < arr.length; fast++) {
if(arr[fast]!=val){
arr[slow] = arr[fast];
slow++;
}
}
return slow;
}
/**
* 26. 原地删除有序数组中的所有重复项
* 输入:nums = [1,1,2]
* 输出:2, nums = [1,2]
*
* 输入:nums = [0,0,1,1,1,2,2,3,3,4]
* 输出:5, nums = [0,1,2,3,4]
* @param arr
* @return
*/
public int removeDup(int[] arr){
int slow= 0;
for (int fast = 1; fast < arr.length; fast++) {
if(arr[slow]!=arr[fast]){
arr[++slow] = arr[fast];
}
}
return slow+1;
}
75、颜色分类
采用双指针:加深理解指针确定位置进行交换的操作
public void sortColors(int[] nums) {
int p = 0;//作为0的分割指针
for (int i = 0; i < nums.length; ++i) {//与i++功能一致但是性能相对更好
if(nums[i]==0){
int temp = nums[i];
nums[i] = nums[p];
nums[p] = temp;
++p;
}
}
// 2.在从p开始重现交换1 和 2 的位置
for (int i = p; i < nums.length; ++i) {
if(nums[i]==1){
int temp = nums[i];
nums[i] = nums[p];
nums[p] = temp;
++p;
}
}
}
88. 合并两个有序数组:如何将数组所有元素整体后移,防止数组覆盖?
167. 两数之和 II - 输入有序数组(有序数列的首尾双指针)
125. 验证回文串
345. 反转字符串中的元音字母
11. 盛最多水的容器:经典题目
209. 长度最小的子数组:滑动窗口
88 、合并两个有序数组
//逆向双指针解决合并两个排序数组
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m-1;
int j = n-1;
int last = m+n-1;
while(i>=0 && j>=0){
if(nums1[i]>nums2[j]){
nums1[last--] = nums1[i--];
}else{
nums1[last--] = nums2[j--];
}
}
// 剩下的部分直接复制过去
while(j>=0){
nums1[last--] = nums2[j--];
}
}
167 、两数之和 II - 输入有序数组
//给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target
/**
* 167. 两数之和 II - 输入有序数组
* 递增数列,输出何为目标值的下标
* @param numbers
* @param target
* @return
*/
public int[] twoSum(int[] numbers, int target) {
int left=0,right=numbers.length-1;
while(left<right){
int sum = numbers[left]+numbers[right];
if(sum==target) {
return new int[]{left+1,right+1};
}else if(sum<target){
left++;
}else{
right--;
}
}
return null;
}
125 、验证回文串
// "A man, a plan, a canal: Panama"——>只考虑字母和数字字符,忽略字母的大小写。
public boolean isPalindrome(String s) {
//1.字符串预处理
StringBuilder list = new StringBuilder();
int length = s.length();
for (int i = 0; i < length; i++) {
char ch = s.charAt(i);
if (Character.isLetterOrDigit(ch)) {
list.append(Character.toLowerCase(ch));
}
}
// 2.双指针遍历是否一致
int i = 0;
int j = list.length()-1;
while(i<=j){
if(list.charAt(i)!=list.charAt(j)){
return false;
}else{
i++;
j--;
}
}
return true;
}
345 、反转字符串中的元音字母
//1.将元音字符先存起来
private final static HashSet<Character> vowel = new HashSet<>(
Arrays.asList('a','e','i','o','u','A','E','I','O','U')
);
public String reverseVowels(String s) {
char[] ch = new char[s.length()];
//1.双指针前后遍历字符串
int left = 0;
int right = s.length()-1;
while(left<=right){
char cleft = s.charAt(left);
char cright = s.charAt(right);
//如果是非元音字符,则直接添加到新的字符数组
if(!vowel.contains(cleft)){
ch[left++]=cleft;
}else if(!vowel.contains(cright)){
ch[right--] = cright;
}else{
//如果是元音字符,则交换位置放
ch[left++] = cright;
ch[right--] = cleft;
}
}
return new String(ch);
}
11 、 盛最多水的容器
public int maxArea(int[] height) {
// 1.思路:双指针,移动小的那边
int left = 0,right = height.length-1;
int res = 0;
while(left<right){
res = height[left] < height[right] ?
Math.max(res, (right - left) * height[left++]):
Math.max(res, (right - left) * height[right--]);
}
return res;
}
209 、长度最小的子数组
//我们把数组中的右指针右移,直到总和大于等于 target 为止,记录个数。然后左指针右移,直到队列中元素的和小于 target为止,记录个数。重复,直到右指针到达队尾。
public int minSubArrayLen(int target, int[] nums) {
int i = 0,j=0,sum=0;
int res = Integer.MAX_VALUE;
while(j<nums.length){
//1.右指针滑动
sum+=nums[j++];
while(sum>=target){
//2.固定右指针,滑动左指针,求最小的子数组
res = Math.min(res,j-i);
sum-=nums[i++];
}
}
return res==Integer.MAX_VALUE?0:res;
}
56 、合并区间
//以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
public int[][] merge(int[][] intervals) {
// 1.创建一个集合存储答案
List<int[]> ans = new ArrayList<>();
// 2.判空直接返回
if (intervals == null || intervals.length == 0){
return ans.toArray(new int[0][]);
}
// 3.将集合按数组一维进行排序
Arrays.sort(intervals,(a,b)->{a[0]-b[0]});
for (int i = 0; i < intervals.length; i++) {
int start = intervals[i][0];
int end = intervals[i][1];
// 4.如ans中最后一个元素的end>遍历元素的start,要进行合并操作
if(ans.size()==0 ||ans.get(ans.size()-1)[1]<start){
ans.add(intervals[i]);
}else{
ans.get(ans.size()-1)[1] = Math.max(ans.get(ans.size()-1)[1],end);
}
}
return ans.toArray(new int[ans.size()][]);
}
总结:越是简单的题目,想考察的越深,不是为了考察简单的思路。
//在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix.length == 0){
return false;
}
//从右上角进行遍历,这样就可以直接进行区分,有点类似二叉树的操作
int i=matrix.length-1,j=0;
while(i>=0 && j<matrix[0].length){
// 可以进一步二分法进行操作,但是感觉没有必要
if(matrix[i][j]==target){
return true;
}
if(matrix[i][j]<target){
j++;
}else{
i--;
}
}
return false;
}
剑指 Offer 11. 旋转数组的最小数字
//把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 `[3,4,5,1,2]` 为 `[1,2,3,4,5]` 的一个旋转,该数组的最小值为1。
public int minArray(int[] numbers) {
// 最好的解法是必须考虑到输入本身的特点,结合输入的特点进行求解,避免暴力求解
int low = 0;
int high = numbers.length-1;
while(low<high){
int pivot = low + (high-low)/2;
if(numbers[pivot]<numbers[high]){
high = pivot;
}else if(numbers[pivot]>numbers[high]){
low = pivot+1;
}else{
// 比如 [1 3 3],此时 numbers[pivot] = numbers[high]=3
// 如果不另外计算,直接low = pivot+1,就会错过最小值
// 而右边的值总是比中值大。
high -=1;
}
}
return numbers[low];
}
剑指 Offer 17. 打印从1到最大的n位数
//输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
//这里考察,字符串的加法,因为不论用那种基本数据类型都会出现超过表示范围的风险
//bilibili面试中就出现了字符串加法中按位进位的情况
public class _17_printNumbers {
int[] res;
int count=0;
char[] num,loop={'0','1','2','3','4','5','6','7','8','9'};
public int[] printNumbers(int len){
// 1.初始化
res = new int[(int)Math.pow(10,len)-1];
num = new char[len];
// 2.从个位开始递归
for (int i = 1; i <=len; i++) {
dfs(0,i);
}
return res;
}
public void dfs(int index,int len){
//3.基线
if(index==len){
res[count++]=Integer.parseInt(new String(num,0,len));
return;
}
int start = 0;
if(index==0){
start=1;
}
// 4.每一位下进行递归全排列
for (int i = start; i <10 ; i++) {
num[index] = loop[i];
dfs(index+1,len);
}
}
}
【剑指Offer】21、调整数组顺序使奇数位于偶数前面
//仿快排收尾双指针
public int[] exchange(int[] nums) {
int i = 0,j = nums.length-1;
while(i<j){
while(i<j && nums[j]%2==0){
j--;
}
while(i<j && nums[i]%2!=0){
i++;
}
swap(nums,i,j);
}
return nums;
}
public void swap(int[] nums,int i,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
【剑指Offer】29、顺时针打印矩阵
public int[] spiralOrder(int[][] matrix) {
//如果没有这一步判空,当输入数组为空时,也会直接进入循环,导致outofIndex
if(matrix.length==0){
return new int[0];
}
int left=0,right = matrix[0].length-1;
int top=0,bottom = matrix.length-1;
int index=0;
int[] res = new int[matrix.length*matrix[0].length];
while(true){
// 上层遍历
for (int i = left; i <=right ; i++) {
res[index++] = matrix[top][i];
}
if(++top>bottom){
break;
}
// 右边遍历
for (int i = top; i <=bottom ; i++) {
res[index++] = matrix[i][right];
}
if(--right<left){
break;
}
// 下边遍历
for (int i = right; i >=left ; i--) {
res[index++] = matrix[bottom][i];
}
if(--bottom<top){
break;
}
// 左边遍历
for (int i = bottom; i >=top ; i--) {
res[index++] = matrix[i][left];
}
if(++left>right){
break;
}
}
return res;
}
//1.利用HashSet去统计次数
//2.排序,中间的那个数便是
//3.分治算法
//4.摩尔投票法,摩尔投票法 简称同归于尽法,设众数+1,非众数-1,前后和为0之后,剩下的必定含有众数
class Solution {
//数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
public int majorityElement(int[] nums) {
int more = 0, votes = 0;
for(int num : nums){//遍历当前所有元素
if(votes == 0) more = num;//如果前后的和为0,重新设定众数的值为当前值
votes += num == more ? 1 : -1;
}
// 如果题目并没有给定说一定存在众数,则需要多一步验证
int count = 0;
for(int num : nums){//遍历当前所有元素
if(num == x) count+=1;//如果前后的和为0,重新设定众数的值为当前值
}
return count>nums.length/2?x:0;
}
}
// 分治算法
// 1.不断的递归,基线是长度为1,返回这个数字
// 2.左右两个递归的数字是否相等,所以要加范围
// * 如果相等,这个就是众数直接返回
// * 如果不相等,那么就需要算个数,返回个数多的
public int majorityElement2(int[] nums) {
return majorityElementRange(nums,0,nums.length-1);
}
public int majorityElementRange(int[] nums,int begin,int end ) {
// 递归基线
if(begin==end){
return nums[begin];
}
int mid = begin + (end - begin)/2;
int left = majorityElementRange(nums,begin,mid);
int right = majorityElementRange(nums,mid+1,end);
if(left==right){
return left;
}else{
int leftCount = count(nums,left,begin,mid);
int rightCount = count(nums,right,mid+1,end);
return leftCount>rightCount?left:right;
}
}
public int count(int[] nums,int number,int begin,int end){
int count=0;
for (int i = begin; i <= end; i++) {
if(nums[i]==number){
count+=1;
}
}
return count;
}
// 结合归并排序的操作
// 合并两个排序数组 的过程,而每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着 「左子数组当前元素 至 末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」,新增计数操作
class Solution {
int count;
public int reversePairs(int[] nums) {
this.count = 0;
merge(nums, 0, nums.length - 1);
return count;
}
public void merge(int[] nums,int left,int right){
if(left<right){
int mid = left + ((right-left)>>1);
merge(nums,left,mid);
merge(nums,mid+1,right);
mergeSort(nums,left,mid+1,right);
}
}
public void mergeSort(int[] nums,int left,int mid,int right){
// 1.重构两个子数组
int[] leftArr = new int[mid-left];
for (int i = 0; i < leftArr.length; i++) {
leftArr[i] = nums[left+i];
}
int[] rightArr = new int[right-mid+1];
for (int i = 0; i < rightArr.length; i++) {
rightArr[i] = nums[mid+i];
}
// 2.双指针遍历两个数组,比较大小
int i=0,j=0;
while(i<leftArr.length && j<rightArr.length){
if(leftArr[i]<=rightArr[j]){
nums[left++] = leftArr[i++];
}else{
// 加上前半部分数组的索引之后的元素个数
count+=(leftArr.length-i);
nums[left++] = rightArr[j++];
}
}
while(i<leftArr.length){
nums[left++] = leftArr[i++];
}
while(j<rightArr.length){
nums[left++] = rightArr[j++];
}
}
}
【剑指 Offer】 56 - I. 数组中数字出现的次数
//异或的操作:如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0,相同的数字异或为0
//两数相等异或结果为0,一个数与0异或结果就等于其本身。所以如果数组中只有一个出现一次的数
//那么就只需要对所有的数进行异或就可以得到这个只出现一次的数
//那么也就来了思路,如果我们能把两个不相等的数字,分别分到两个子数组中,再在子数组中进行异或操作,就能把两个不相等的数找出来
//1.分数组,首先确定两个数字之中不一样的位数是哪一位
//2.在根据这个固定出来的位数进行分数组异或操作
//3.bingo,得到结果
一位数字不一样的异或
两位数字不一样的异或
public class Solution{
public int[] singleNumbers(int[] nums) {
//1.先求出所有数字异或操作之后得到的结果
int res=0;
for (int num : nums) {
res^=num;
}
//2.确定异或结果中是哪一个二进制为不一样
int m = 1;
//若 a & 0001 = 1 ,则 a 的第一位为 1 ;
//若 a & 0010 = 1 ,则 a 的第二位为 1 ;
//左移m的1,0001——0010——0100——1000
while((res&m)==0) m<<=1;
//3.根据确定的不同的位来进行划分子数组
int x=0,y=0;
for (int num : nums) {
if((num&m)!=0) x^=num;//一个数组中异或得到的结果就是x
else y^=num;//另一个数组中异或得到结果y
}
//4.返回结果
return new int[]{x,y};
}
}
【剑指 Offer 】56 - II. 数组中数字出现的次数 II
//找出在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
//[1,1,1,3]-->3
public int singleNumber(int[] nums) {
//1.hashMap直接统计次数
//2.计算对应二进制位中的1的个数,在对3取余,得到的结果就是只出现一次的结果,但是过程比较复杂,留作以后再看吧。
}
【剑指 Offer】 57、和为s的两个数字
// 1.HashMap将所有数存起来,固定一个,getValue是否有
// 2.递增数组,首尾双指针
public int[] twoSum(int[] nums, int target) {
int i = 0,j = nums.length-1;
while(i<j){
int num = nums[i],ans = target-num;
while(i<j && nums[j]>ans){
j--;
}
if(nums[j]==ans){
return new int[]{i,j};
}else{
i++;
j = nums.length-1;//todo,这里可以继续优化
}
}
return null;
}
//优化
public int[] twoSum2(int[] nums, int target) {
int i = 0, j = nums.length - 1;
while(i < j) {
int s = nums[i] + nums[j];
if(s < target) i++;
else if(s > target) j--;
else return new int[] { nums[i], nums[j] };
}
return new int[0];
}