剑指offer6-面试中的各项能力

题目-数字在排序数组中出现的次数

统计一个数字在排序数组中出现的次数。 
如Input:
  array=1,2,3,3,3,3,4,6
  k=3
Output:
  4

思路

1.由于输入的数组是排序的,那么二分查找算法很适用这个场景。二分查找很容易找到一个3,由于3可能出现很多次,因此我们可以在3的左右两边按顺序扫描,分别找到第一个3和最后一个3.

因为要查找的数字在长度为n的数组中有可能出现O(n)次,所以顺序扫描的时间复杂度为O(n)

2.优化的解法是:用二分查找算法直接找到第一个k和最后一个k

思路就是:(以找到第一个k为例)二分查找总是拿数组中间的数字和k作比较,如果中间的数字比k大,那么k只有可能出现在数组的前半段,下一轮只要在数组的前半段查找就好了。反之。

如果中间数字==k,那么先判断这个数字是不是第一个k。如果位于中间数字的前一个数不是k,那么说明中间数就是第一个k;如果前一个数也是k,说明第一个k肯定在数组的前半段,下一轮依然需要在数组的前半段查找。

这样查找第一个k和最后一个k都是,使用二分查找法在数组中查找一个合乎要求的数字,时间复杂度都是O(logn)。

解法


class Solution {
    public int search(int[] array, int k) {
        /*int ret=0;
        for(int i=0;i<nums.length;i++){
            if(nums[i]==target){
                ret++;
            }
        }
        return ret;*/
        if(array.length == 0)
            return 0;
        int left=0;
        int right=array.length-1;
        int index=0;
        //二分查找,找到left,即找到第一个k
        while(left<right){
            int mid = (left+right)/2;
            if(array[mid] >= k){
                right = mid;
            }
            else if(array[mid]<k){
                left = mid+1;
            }
            
        }        
        //遍历查找k
        int count=0;
        while(left<array.length && array[left++]==k)
            count++;
        return count;
    }
}


public
class Solution {
//这种解法在leetcode给的例子里,{1,2,3}&3,这个样例不能输出正确结果;当循环条件改为while(left<=right)时,则{1,1,2}&2样例不能正确输出结果
public int GetNumberOfK1(int [] array , int k) { if(array.length == 0) return 0; int left=0; int right=array.length-1; int index=0; //二分查找,找到靠中间的那个k while(left<right){ int mid = (left+right)/2; if(array[mid] == k){ index = mid; break; } else if(array[left]<array[mid]){ left = mid+1; } else{ right = mid-1; } } //遍历查找k int count=0; //从找到的index,往前查找 for(int i=index; i>=0 && array[i]==k; i--) count++; //从找到的index,往后查找 for(int i=index+1; i<array.length && array[i]==k; i++) count++; return count; }

    public int GetNumberOfK2(int [] array , int k) {
        if(array==null)
            return 0;
        int first=GetFirstK(array, array.length, k, 0, array.length-1);
        int last=GetLastK(array, array.length, k, 0, array.length-1);
        int index=0;
        if(first>-1 && last>-1){
            index=last-first+1;
        }
        return index;
    }
   
    public int GetFirstK(int[] array, int length, int k, int left, int right){
        if(left>right){
            return -1;
        }
        int mid=(left+right)/2;
        //int midData=array[mid];
        if(array[mid]==k){
            if((mid>0 && array[mid-1]!=k)|| mid==0){
                return mid;
            }
            else{
                right=mid-1;
            }
         }
         else if(array[mid]>k){
             right=mid-1;
         }
         else{
             left=mid+1;
         }
         return GetFirstK(array, length, k, left, right);
    }
   
    public int GetLastK(int[] array, int length, int k, int left, int right){
        if(left>right){
            return -1;
        }
        int mid=(left+right)/2;
        //int midData=array[mid];
        if(array[mid]==k){
            if((mid<length-1 && array[mid+1]!=k)|| mid==length-1){
                return mid;
            }
            else{
                left=mid+1;
            }
         }
         else if(array[mid]>k){
             right=mid-1;
         }
         else{
             left=mid+1;
         }
         return GetLastK(array, length, k, left, right);
    } }

 

题目-二叉树的深度

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 

思路

(该树的深度为4,由于从根结点到叶结点最长的路径包含4个结点)

如果一棵树只有一个结点,那么深度为1;

如果根结点只有左子树没有右子树,那么树的深度应该是左子树深度+1;

反之,深度应该是右子树深度+1;

如果既有左子树又有右子树,那么就是深度的较大值+1.

可以使用递归来实现。

解法

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public int TreeDepth(TreeNode root) {
        if(root == null)
            return 0;
        int left = TreeDepth(root.left);
        int right = TreeDepth(root.right);
        return (left>right)?(left+1):(right+1);
    }
}

 

题目-平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

思路

平衡二叉树的概念是:如果某二叉树中任意结点的左右子树的深度相差不超过1。

1.最简单的思路就是直接在遍历树的每个结点的时候,调用上面的求树深度的函数得到结点的 左右子树 的深度。如果某结点的左右子树深度相差超过1,则不是平衡树。

这种方式出现一个结点会被重复遍历多次的情况,导致时间效率不高。

2.因此考虑每个结点只遍历一次的解法

如果用后序遍历的方式遍历二叉树的每个结点,在遍历到一个结点之前,已经遍历到它的左右子树。

那么只要在遍历每个结点的时候记下它的深度,就可以一边遍历一边判断每个结点是不是平衡的。

解法

public class Solution {
    private boolean isBalanced=true;
    public boolean IsBalanced_Solution(TreeNode root) {
        height(root);
        return isBalanced;
    }
    private int height(TreeNode root){
        if(root==null || !isBalanced)
            return 0;
        int left = height(root.left);
        int right = height(root.right);
        //平衡树中任意结点的左右子树的深度相差不超过1
        if(Math.abs(left-right)>1)
            isBalanced = false;
        return 1+Math.max(left, right);
    }
}

 

题目-数组中只出现一次的数字

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。 

思路

1.我们首先可以考虑如果只有一个数字出现了一次,那么考虑异或的性质:任何一个数字异或它自己都等于0.即从头到尾遍历数组,最后的结果是只出现一次的数字,成对出现两次的数字全部在异或中抵消了。

2.那么有了上面的思路。对于原题,我们可以试图把原数组拆分成两个子数组,使每个子数组包含一个只出现一次的数字,其他数字都承兑出现两次。

3.那么如何这样拆分数组呢?

由于不相同的元素在位级表示中必定会有一位不同,那么所有元素异或得到的结果是 不存在重复的两个元素异或的结果。也就是这个结果 的二进制表示中 至少有一位 为1,我们在结果数字中找到第一个为1的位的位置,记为n。

以第n位是不是1为标准,把原数组中的数字分为两个子数组。第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0.

diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。

求相反数:取反+1.(-n=(~n)+1)

解法

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int diff=0;
        //相同的数字异或为0,0与其他数字异或得到原数字
        //全部异或运算得到一个值,此时结果一定非0
        for(int num:array){
            diff^=num;
        }
        //得到diff最右侧不为0的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位可以将两个元素区分开
        //即得到该值二进制最低位1的位置index
        diff &= -diff;
        //按照最低位index是0/1把原数组分为两组
        //每组进行连续异或运算得到只出现一次的数字,因为相同的数会得0
        for(int num:array){
            if((num&diff)==0)
                num1[0]^=num;
            else
                num2[0]^=num;
        }
    }
}
class Solution {
    public int[] singleNumbers(int[] nums) {
        int sum=0;
        for(int num:nums){
            sum^=num;
        }
        //找从右向左数第几位不同,也就是第index位
        int index=0;
        while((sum&1) == 0){
            index++;
            sum>>=1;
        }
        int[] ret=new int[2];
        for(int num:nums){
            if(((num>>index)&1) == 0){
                ret[0]^=num;
            }
            else{
                ret[1]^=num;
            }
        }
        return ret;
    }
}

 

题目-和为S的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。 

思路

使用两个指针,一个指向数组头,一个指向数组尾。

头指针往后遍历,尾指针向前遍历。

不断收拢两个指针的间距,直到符合和为S的要求。

如果多对,需要输出乘积最小的,由于是从头尾开始查找,当前满足条件的头指针位置一定是最小的。因此能得到乘积最小。

解法

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> ret = new ArrayList<Integer>();
        if(array==null)
            return ret;
        int start=0,end=array.length-1;
        while(start<end){
            int curSum=array[start]+array[end];
            //如果和大于,则减小
            if(curSum>sum){
                end--;
            }
            //如果小于,则加
            else if(curSum<sum){
                start++;
            }
            else{
                ret.add(array[start]);
                ret.add(array[end]);
                break;
            }
        }
        return ret;
    }
}

 

题目-和为S的连续正数序列

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck! 

思路

借鉴上题的经验,那我们同样也可以以int设置两个指针start,end,分别表示序列的最小值和最大值。

如果序列之和大于S,那么就从序列中去除较小的数,就增大start的值;

如果和小于S,那么就增加end,在序列中增加更多的数字。

如果和==S,那么就是找到一个序列。首先把它添加进去,然后再增加end和start,把整个序列往后移动一位。再次进行比较和查找

解法

import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
        int start = 1,end = 2;
        int curSum = 3;
        while(end < sum){
            //大于sum,则从序列中去掉较小的值。也就是当前的start增大
            if(curSum > sum){
                curSum=curSum-start;
                start++;
            }
            //小于sum,那么需要让序列包含更多的数字
            else if(curSum < sum){
                end++;
                curSum=curSum+end;
            }
            //找到一个序列
            else{
                ArrayList<Integer> list = new ArrayList<>();
                //由于是连续序列,把序列中的数字加到list中
                for(int i=start; i<=end; i++){
                    list.add(i);
                }
                ret.add(list);
                curSum=curSum-start;
                start++;
                end++;
                curSum=curSum+end;
            }
        }
        return ret;
    }
}

 

题目-左旋转字符串

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! 

思路

对于这个,我们可以列出字符串及其旋转后的效果。那么我们可以发现一些规律:

比如对于序列S="abcXYZdef",可以将其分为两部分,前面的3个字符为第一部分,后面的为第二部分。

然后我们分别反转这两部分,变成cba和fedZYX。

那么再将整个字符串旋转,得到左移3位的效果,“XYZdefabc”

(注意在java中,为了处理单个字符,我们通常需要把String类型转化为char[] )

解法

public class Solution {
    public String LeftRotateString(String str,int n) {
        if(str.length() <= n){
            return str;
        }
        char[] chars = str.toCharArray();
        reverse(chars, 0, n-1);
        reverse(chars, n, str.length()-1);
        reverse(chars, 0, str.length()-1);
        return new String(chars);
    }
     
    private void reverse(char[] chars, int i, int j){
        while(i<j){
            swap(chars, i++, j--);
        }
    }
     
    private void swap(char[] chars, int i, int j){
        char temp = chars[i];
        chars[i] = chars[j];
        chars[j] = temp;
    }
}

 

题目-翻转单词顺序列

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? 

思路

整个题与上一题类似。

同样是使用翻转,那么可以把这个翻转拆分为两步:

1.把每个单词都翻转,使用start和end记录当前这个单词的位置,也就是需要翻转的位置。

2.把整个句子翻转

3.这个翻转过程需要注意:只有一个单词的情况;有标点符号的情况

解法

public class Solution {
    public String ReverseSentence(String str) {
        if(str==null){
            return null;
        }
        int n = str.length();
        char[] chars = str.toCharArray();
        //记录要反转单词的起始位置
        int start=0, end=0;
        for(end=0; end<=n; end++){
            if(end == n || chars[end] == ' '){
                reverse(chars, start, end-1);
                start = end+1;
            }            
        }
        reverse(chars, 0, n-1);
        return new String(chars);
    }
    private void reverse(char[] chars, int i, int j){
        while(i<j){
            swap(chars, i++, j--);
        }
    }
    private void swap(char[] chars, int i, int j){
        char temp = chars[i];
        chars[i] = chars[j];
        chars[j] = temp;
    }
}

 

题目-扑克牌顺子

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。 

思路

1.怎么判断数字是不是连续呢,直观的方法就是将数组排序。

2.由于有癞子的存在,那么可以用癞子(0)去填补数组中的空缺,如果排序后的数组不是连续的,也就是相邻的数字间隔若干个数字,只要我们有足够多的癞子,那就可以去填补这个空缺。

3.那么总结起来过程就是:1)数组排序;2)统计数组中0的个数;3)最后统计排序后的数组中相邻数字间的空缺;4)判断0是否足够去填补空缺。

4.需要注意的是,顺子中不能出现对子,也就是不能出现重复的数字。

解法

import java.util.Arrays;
public class Solution {
    public boolean isContinuous(int [] numbers) {
        if(numbers.length < 5){
            return false;
        }
        Arrays.sort(numbers);
        //统计大小王,当成癞子
        int cnt=0;
        for(int num:numbers){
            if(num==0){
                cnt++;
            }
        }
        //使用癞子补全顺子
        for(int i=cnt;i<numbers.length-1;i++){
            //存在相同的数字,那么就不是顺子
            if(numbers[i+1] == numbers[i])
                return false;
            cnt-=numbers[i+1]-numbers[i]-1;
        }
        //如果能剩下癞子,或刚好够补,那么则是顺子
        return cnt>=0;
    }
}

 

题目-孩子们的游戏(圆圈中最后剩下的数)

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1) 

如果没有小朋友,请返回-1 

思路

1.这是一个经典的约瑟夫环问题,那么通过分析每次被删除的数字的规律 来直接计算圆圈中剩下的数字。

 

最后得到一个递归公式:

要得到n个数字的序列中最后剩下的数,只需要得到n-1个数字的序列中最后剩下的数字。以此类推。

2.模拟小朋友退出游戏的过程,用LinkedList链表来表示圆圈。然后每次按照要求删除结点,((removeIndex+m-1)%list.size();

解法

//解法1
import java.util.LinkedList;
public class Solution { public int LastRemaining_Solution(int n, int m) { if(n == 0) return -1; if(n == 1){ return 0; } int last=0; for(int i=2; i<=n; i++) last=(last+m)%i; return last; }

 

//解法2
import java.util.LinkedList;
public class Solution { public int LastRemaining_Solution(int n, int m) { if(n<1 || m<1){ return -1; } LinkedList<Integer> list = new LinkedList<Integer>(); for(int i=0;i<n;i++){ list.add(i); } //当前删除的结点下标 int removeIndex=0; while(list.size()>1){ //下一个要删除的结点的下标 removeIndex=(removeIndex+m-1)%list.size(); list.remove(removeIndex); } return list.getFirst(); } }

 

posted @ 2020-03-02 23:20  闲不住的小李  阅读(194)  评论(0编辑  收藏  举报