剑指offer刷题笔记(数组)

剑指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] 即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值