剑指offer刷题笔记(数组)
1.斐波那契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1,n≤39)。例如:输入4,返回3
最终代码:
public class Solution {
int Fibonacci(int n) {
if(n == 0)
return 0;
if(n == 1)
return 1;
int numfn1 = 0, numfn2 = 1;
int currentnum=0;
for(int i=2; i<=n; ++i) {
currentnum = numfn1+numfn2;
numfn1 = numfn2;
numfn2 = currentnum;
}
return currentnum;
}
};
分析:斐波那契数列公式:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
每次计算只用到两个数的值
currentnum:第n项;
numfn1:第n-1项;
numfn2:第n-2项。
2.数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组[1,2,3,2,2,2,5,4,2]。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。你可以假设数组是非空的,并且给定的数组总是存在多数元素。1<=数组长度<=50000
输入:[1,2,3,2,2,2,5,4,2]
返回值:2
最终代码:
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length == 0)return 0;
int preValue = array[0];//用来记录上一次的记录
int count = 1;//preValue出现的次数(相减之后)
for(int i = 1; i < array.length; i++){
if(array[i] == preValue)
count++;
else{
count--;
if(count == 0){
preValue = array[i];
count = 1;
}
}
}
int num = 0;//需要判断是否真的是大于1半数,这一步骤是非常有必要的,因为我们的上一次遍历只是保证如果存在超过一半的数就是preValue,但不代表preValue一定会超过一半
for(int i=0; i < array.length; i++)
if(array[i] == preValue)
num++;
return (num > array.length/2)?preValue:0;
}
}
分析:其实是找数组中的众数。
遍历数组,preValue是数组中第一个值,count记录preValue出现的次数
出现一次count加1,如果下次没出现就减1,继续遍历
count减到0就将当前循环到的数组中的数设置为preValue,count置1
最后判断preValue是否超过数组一半。
3.数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任一一个重复的数字。 例如,如果输入长度为7的数组[2,3,1,0,2,5,3],那么对应的输出是2或者3,存在不合法的输入的话输出-1。
输入:[2,3,1,0,2,5,3]
返回值:2
说明:2或3都是对的
最终代码:
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param numbers int整型一维数组
* @return int整型
*/
public int duplicate (int[] numbers) {
// write code her
int[] count=new int[numbers.length];
for(int i=0;i<numbers.length;i++){
count[numbers[i]]++;
if(count[numbers[i]]==2){
return numbers[i];
}
}
return -1;
}
}
分析:重新建立一个数组,对原数组遍历,记录数组中每个数出现的次数,若等于2就返回。
class Solution {
public int findRepeatNumber(int[] nums) {
int temp;
for(int i = 0;i<nums.length;i++){
while(nums[i] != i){
if(nums[i]==nums[nums[i]]){
return nums[i];
}
temp=nums[i];
nums[i]=nums[temp];
nums[temp]=temp;
}
}
return -1;
}
}
如果没有重复数字,那么正常排序后,数字i应该在下标为i的位置,所以思路是重头扫描数组,遇到下标为i的数字如果不是i的话,(假设为m),那么我们就拿与下标m的数字交换。在交换过程中,如果有重复的数字发生,那么终止返回ture
4.构建乘积数组
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)对于A长度为1的情况,B无意义,故而无法构建,因此该情况不会存在。
输入:[1,2,3,4,5]
返回值:[120,60,40,30,24]
import java.util.ArrayList;
public class Solution {
public int[] multiply(int[] A) {
int[] n = new int[A.length];
for(int i = 0; i < A.length; i++){
int temp = 1;
for(int j = 0; j < A.length; j++){
if(j == i){
continue;
}else {
temp *= A[j];
}
}
n[i] = temp;
}
return n;
}
}
分析:此处利用循环中可以跳出的思想,如果碰到两数组序号相同就跳过,继续下一次计算。
5.二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
最终代码:
public class Solution {
public boolean Find(int target, int [][] array) {
int m = array.length - 1;
int i = 0;
while(m >= 0 && i < array[0].length){
if(array[m][i] > target)
m--;
else if(array[m][i] < target)
i++;
else
return true;
}
return false;
}
}
分析:每行从左至右递增,每列从上至下递增,左下角的数x是当前行最小值,是当前列最大值, x 与target比较,若x>target则向上一行找;若x>target则在当前行找。
6. 调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
输入:[1,2,3,4]
返回值:[1,3,2,4]
最终代码:
int res[]=new int[array.length];
int idx=0;
for(int a:array){
if(a%2==1) res[idx++]=a;
}
for(int a:array){
if(a%2==0) res[idx++]=a;
}
return res;
可以暴力新建两个arraylist分别存放奇数偶数,存放时是有序的,然后遍历list放入新数组。
代码中是直接遍历数组,先将奇数加入新数组,后将偶数加入新数组。
class Solution {
public int[] exchange(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
while (left < right && nums[left] % 2 != 0) {
left++;
}
while (left < right && nums[right] % 2 == 0) {
right--;
}
if (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
return nums;
}
}
一次快排
7. 数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
代码:
public class Solution {
//全局变量记录逆序对个数
int count;
//总体思想:分治
public int InversePairs(int[] array) {
//判断数组长度,大于0继续分割
if (array.length != 0) {
//第一次划分数组
divide(array, 0, array.length - 1);
}
return count;
}
//分治中的分
public void divide(int[] arr, int start, int end) {
//首先判断递归是否中止
if (start >= end) {
return;
}
//找出数组中间值
int mid = start + (end - start) / 2;
//递归调用divide划分数组
divide(arr, start, mid);
divide(arr, mid + 1, end);
//记录数组逆序对
merge(arr, start, mid, end);
}
public void merge(int[] arr, int start, int mid, int end) {
//存放排序后的新数组
int[] temp = new int[end - start + 1];
int i = start, j = mid + 1, k = 0;
//两两比较,若前面的数大于后面的数就构成逆序对
while (i <= mid && j <= end) {
//若前面的数小于后面的数,直接存入新数组,并且移动指针至下一点
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
//若arr[i]>arr[j],那么这次从arr[i]开始直到arr[mid]都大于arr[j]
//因为分治的数组两边已经排过序
count = (count + mid - i + 1) % 1000000007;
}
}
//如果剩下还有没比完的,直接按顺序放入数组,先放左侧,后放右侧数组
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= end) {
temp[k++] = arr[j++];
}
//将原先的arr数组覆盖成新的排序后的数组,左右侧都是
for (k = 0; k < temp.length; k++) {
arr[start + k] = temp[k];
}
}
}
分析:将数组从中间划分,划分至左右数组各只有一个数为止,开始比较大小。若左侧数组中的数大于右侧数组对应的数,则两数对换,逆序对数+1。总逆序对数等于左侧数组逆序对数+右侧数组逆序对数+左侧比右侧多的逆序对数。
8. 数字在升序数组中出现的次数
统计一个数字在升序数组中出现的次数。
public class Solution {
int count;
public int GetNumberOfK(int [] array , int k) {
if(array.length==0){
return 0;
}else if(array.length==1){
if(array[0]==k){
count=1;
}
return count;
}else{
divide(array,0,array.length-1,k);
return count;
}
}
public void divide(int[] arr,int start,int end,int k){
if(start>=end){
return;
}else{
int mid=start+(end-start)/2;
if(arr[mid]<k){
divide(arr,mid+1,end,k);
}else if(arr[mid]>k){
divide(arr,start,mid,k);
}else {
for(int i = start;i<end-start+1;i++){
if(arr[i]==k){
count++;
}
}
}
}
}
}
由于为升序数组,考虑使用二分算法,先判断数组是否为空,找出中间值mid进行比较,若大于mid则划分后半部分数组,以此类推。若等于中间值则循环遍历mid所在的数组,计算数出现的次数。
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
int low=0,high=len-1;
while(low<=high){
int mid = low+(high-low)/2;
if(nums[mid]<target){
low=mid+1;
}else if(nums[mid]>target){
high=mid-1;
}else{
if(nums[low]!=target){
low++;
}else if(nums[high]!=target){
high--;
}else{
break;
}
}
}
return high-low+1;
}
}
更直观一点,只考虑mid的值是否为目标值,如果不是,则分左右继续判断,如果是,则判断low与high索引处的值是不是与目标值相同,用下标值的差加一计算。
9. 和为S的两个数字
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,返回两个数的乘积最小的,如果无法找出这样的数字,返回一个空数组即可。
对应每个测试案例,输出两个数,小的先输出。
class Solution {
public int[] twoSum(int[] nums, int target) {
if(nums.length<2){
return null;
}
int len=nums.length;
int left=0;
int right=len-1;
int[] result=new int[2];
while(left<right){
if(nums[left]+nums[right]<target){
left++;
}
if(nums[left]+nums[right]>target){
right--;
}
if(nums[left]+nums[right]==target){
result[0]=nums[left];
result[1]=nums[right];
break;
}
}
return result;
}
}
10.把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
import java.util.ArrayList;
public class Solution {
public String PrintMinNumber(int [] numbers) {
String str = "";
for (int i=0; i<numbers.length; i++){
for (int j=i+1; j<numbers.length; j++){
//将两个数拼接
int a = Integer.valueOf(numbers[i]+""+numbers[j]);
int b = Integer.valueOf(numbers[j]+""+numbers[i]);
if (a > b){
int t = numbers[i];
numbers[i] = numbers[j];
numbers[j] = t;
}
}
}
for (int i = 0; i < numbers.length; i++) {
str += String.valueOf(numbers[i]);
}
return str;
}
}
在这里自定义一个比较大小的函数,比较两个字符串s1, s2大小的时候,先将它们拼接起来,比较s1+s2,和s2+s1那个大,如果s1+s2大,那说明s2应该放前面,所以按这个规则,s2就应该排在s1前面。
11.顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵:
[[1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,16]]
则依次打印出数字
[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10]
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> list = new ArrayList<>();
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return list;
}
int up = 0;
int down = matrix.length-1;
int left = 0;
int right = matrix[0].length-1;
while(true){
// 最上面一行
for(int col=left;col<=right;col++){
list.add(matrix[up][col]);
}
// 向下逼近
up++;
// 判断是否越界
if(up > down){
break;
}
// 最右边一行
for(int row=up;row<=down;row++){
list.add(matrix[row][right]);
}
// 向左逼近
right--;
// 判断是否越界
if(left > right){
break;
}
// 最下面一行
for(int col=right;col>=left;col--){
list.add(matrix[down][col]);
}
// 向上逼近
down--;
// 判断是否越界
if(up > down){
break;
}
// 最左边一行
for(int row=down;row>=up;row--){
list.add(matrix[row][left]);
}
// 向右逼近
left++;
// 判断是否越界
if(left > right){
break;
}
}
return list;
}
}
不断地收缩矩阵的边界
定义四个变量代表范围,up、down、left、right
向右走存入整行的值,当存入后,该行再也不会被遍历,代表上边界的 up 加一,同时判断是否和代表下边界的 down 交错
向下走存入整列的值,当存入后,该列再也不会被遍历,代表右边界的 right 减一,同时判断是否和代表左边界的 left 交错
向左走存入整行的值,当存入后,该行再也不会被遍历,代表下边界的 down 减一,同时判断是否和代表上边界的 up 交错
向上走存入整列的值,当存入后,该列再也不会被遍历,代表左边界的 left 加一,同时判断是否和代表右边界的 right 交错
12.重建二叉树
给定某二叉树的前序遍历和中序遍历,请重建出该二叉树并返回它的头结点。
例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}
输入:
[1,2,4,7,3,5,6,8],[4,7,2,1,5,3,8,6]
返回值:
{1,2,3,4,5,6,7,8}
说明:
返回根节点,系统会输出整颗二叉树对比结果
import java.util.Arrays;
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
//数组长度为0的时候要处理
if(pre.length == 0){
return null;
}
int rootVal = pre[0];
//数组长度仅为1的时候就要处理
if(pre.length == 1){
return new TreeNode(rootVal);
}
//我们先找到root所在的位置,确定好前序和中序中左子树和右子树序列的范围
TreeNode root = new TreeNode(rootVal);
int rootIndex = 0;
for(int i=0;i<in.length;i++){
if(rootVal == in[i]){
rootIndex = i;
break;
}
}
//递归,假设root的左右子树都已经构建完毕,那么只要将左右子树安到root左右即可
//这里注意Arrays.copyOfRange(int[],start,end)是[)的区间
root.left = reConstructBinaryTree(Arrays.copyOfRange(pre,1,rootIndex+1),Arrays.copyOfRange(in,0,rootIndex));
root.right = reConstructBinaryTree(Arrays.copyOfRange(pre,rootIndex+1,pre.length),Arrays.copyOfRange(in,rootIndex+1,in.length));
return root;
}
}
因为是树的结构,一般都是用递归来实现。
用数学归纳法的思想就是,假设最后一步,就是root的左右子树都已经重建好了,那么我只要考虑将root的左右子树安上去即可。
根据前序遍历的性质,第一个元素必然就是root,那么下面的工作就是如何确定root的左右子树的范围。
根据中序遍历的性质,root元素前面都是root的左子树,后面都是root的右子树。那么我们只要找到中序遍历中root的位置,就可以确定好左右子树的范围。
正如上面所说,只需要将确定的左右子树安到root上即可。递归要注意出口,假设最后只有一个元素了,那么就要返回。
Arrays.copyOfRange(T[ ] original,int from,int to):将一个原始的数组original,从下标from开始复制,复制到上标to,生成一个新的数组。
注意这里包括下标from,不包括上标to。
0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
class Solution {
public int missingNumber(int[] nums) {
int i = 0, j = nums.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(nums[m] == m) i = m + 1;
else j = m - 1;
}
return i;
}
}
跟排序数组有关的搜索问题,首先想到二分法
旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。
输入:[3,4,5,1,2]
输出:1
class Solution {
public int minArray(int[] numbers) {
int i = 0;
int j = numbers.length-1;
while(i<j){
int mid=i+(j-i)/2;
if(numbers[mid]>numbers[j]){
i=mid+1;
}else if(numbers[mid]<numbers[j]){
j=mid;
}else{
j--;
}
}
return numbers[i];
}
}
初始化: 声明 ii, jj 双指针分别指向 numsnums 数组左右两端;
循环二分: 设 m = (i + j) / 2m=(i+j)/2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i \leq m < ji≤m<j ),可分为以下三种情况:
当 nums[m] > nums[j]nums[m]>nums[j] 时: mm 一定在 左排序数组 中,即旋转点 xx 一定在 [m + 1, j][m+1,j] 闭区间内,因此执行 i = m + 1i=m+1;
当 nums[m] < nums[j]nums[m]<nums[j] 时: mm 一定在 右排序数组 中,即旋转点 xx 一定在[i, m][i,m] 闭区间内,因此执行 j = mj=m;
当 nums[m] = nums[j]nums[m]=nums[j] 时: 无法判断 mm 在哪个排序数组中,即无法判断旋转点 xx 在 [i, m][i,m] 还是 [m + 1, j][m+1,j] 区间中。解决方案: 执行 j = j - 1j=j−1 缩小判断范围,分析见下文。
返回值: 当 i = ji=j 时跳出二分循环,并返回 旋转点的值 nums[i]nums[i] 即可。