剑指offer思路与实现总结(Java版)

##节选剑指offer比较经典和巧妙的一些题目,以便复习使用。一部分题目给出了完整代码,一部分题目比较简单直接给出思路。可是不保证我说的思路都是正确的,我的对算法也不是特别在行,只不过这本书的算法多看了几遍多作了几遍多了点心得体会。因而想总结一下。若是有错误也但愿能指出,谢谢。java

#具体代码能够参考个人GitHub仓库:node

#https://github.com/h2pl/SwordToOffergit

数论和数字规律

从1到n整数中1出现的次数

题目描述
求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有一、十、十一、十二、13所以共出现6次,可是对于后面问题他就没辙了。ACMer但愿大家帮帮他,并把问题更加广泛化,能够很快的求出任意非负整数区间中1出现的次数。程序员

1暴力办法,把整数转为字符串,依次枚举相加。复杂度是O(N * k)k为数字长度。github

2第二种办法看不懂,须要数学推导,太长不看web

排数组排成最小的数

输入一个正整数数组,把数组里全部数字拼接起来排成一个数,打印能拼接出的全部数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。面试

解析:本题的关键是,两个数如何排成最小的,答案是,若是把数字当作字符串a,b那么若是a+b>b+a,则a应该放在b后面。
例如 3和32 3 + 32 = 332,32 + 3 = 323,332>323,因此32要放在前面。算法

根据这个规律,构造一个比较器,使用排序方法便可。编程

丑数

题目描述
把只包含因子二、3和5的数称做丑数(Ugly Number)。例如六、8都是丑数,但14不是,由于它包含因子7。 习惯上咱们把1当作是第一个丑数。求按从小到大的顺序的第N个丑数。数组

解析

1 暴力枚举每一个丑数,找出第N个便可。

2 这个思路比较巧妙,因为丑数必定是由2,3,5三个因子构成的,因此咱们每次构造出一个比前面丑数大可是比后面小的丑数,构造N次便可。

public class Solution {
	        public static int GetUglyNumber_Solution(int index) {
	                if (index == 0) return 0;
	                int []res = new int[index];
	                res[0] = 1;
	                int i2,i3,i5;
	                i2 = i3 = i5 = 0;
	                for (int i = 1;i < index;i ++) {
	                    res[i] = Math.min(res[i2] * 2, Math.min(res[i3] * 3, res[i5] * 5));
	                    if (res[i] == res[i2] * 2) i2 ++;
	                    if (res[i] == res[i3] * 3) i3 ++;
	                    if (res[i] == res[i5] * 5) i5 ++;
	                }
	                return res[index - 1];
	            }
	        }
	    }
	    i2,i3,i5分别表明目前有几个2,3,5的因子,每次选一个最小的丑数,而后开始找下一个。固然i2,i3,i5也要跟着变。

数组和矩阵

二维数组的查找

/**
 * Created by 周杰伦 on 2018/2/25.
 * 题目描述
 在一个二维数组中,每一行都按照从左到右递增的顺序排序,
 每一列都按照从上到下递增的顺序排序。请完成一个函数,
 输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
 1 2 3
 2 3 4
 3 4 5
 */
 
 解析:比较经典的一题,解法也比较巧妙,因为数组从左向右和从上到下的都是递增的,因此找一个数能够先从最右开始找。
 假设最右值为a,待查数为x,那么若是x < a说明x在a的左边,往左找便可,若是x > a,说明x 在 a的下面一行,到下面一行继续按照该规则查找,就能够遍历全部数。
 
算法的时间复杂度是O(M * N)
 
public class 二维数组中的查找 {
    public static boolean Find(int target, int[][] array) {

        if(array[0][0] > target) {
            return false;
        }

        int row = 0;
        int col = 0;
        while (row < array.length && col >0) {
            if (target == array[row][col]) {
                return true;
            }
            else if (target <array[row][col]) {
                col --;
            }
            else if (target > array[row][col]) {
                col ++;
            }
            else row++;
        }
        return false;
    }
}

顺时针打印矩阵。

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每个数字,例如,若是输入以下矩阵: 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.

这题仍是有点麻烦的,由于要顺时针打印,因此其实是由外向内打印,边界的处理和递归调用须要谨慎。

这题我本身没写出标准答案。参考一个答案吧。关键在于四个循环中的分界点设置。

//主体循环部分才5行。实际上是有规律可循的。将每一层的四个边角搞清楚就能够打印出来了
        
    import java.util.ArrayList;
    public class Solution {
        public ArrayList<Integer> printMatrix(int [][] array) {
            ArrayList<Integer> result = new ArrayList<Integer> ();
            if(array.length==0) return result;
            int n = array.length,m = array[0].length;
            if(m==0) return result;
            int layers = (Math.min(n,m)-1)/2+1;//这个是层数
            for(int i=0;i<layers;i++){
                for(int k = i;k<m-i;k++) result.add(array[i][k]);//左至右
                for(int j=i+1;j<n-i;j++) result.add(array[j][m-i-1]);//右上至右下
                for(int k=m-i-2;(k>=i)&&(n-i-1!=i);k--) result.add(array[n-i-1][k]);//右至左
                for(int j=n-i-2;(j>i)&&(m-i-1!=i);j--) result.add(array[j][i]);//左下至左上
            }
            return result;       
        }
    }

调整数组中数字的顺序,使正数在负数的前面

双指针便可以解决,变式有正负,奇偶等等。

数组中出现次数超过一半的数字

本题有不少种解法。

1 最笨的解法,统计每一个数的出现次数,O(n2)

2 使用hashmap,空间换时间O(n)

3 因为出现超过一半的数字必定也是中位数,因此能够先排序,再找到第n/2位置上的节点。

4 使用快速排序的复杂度是O(nlogn),基于快排的特性,每一轮的过程都会把一个数放到最终位置,因此咱们能够判断一下这个数的位置是否是n/2,若是是的话,那么就直接返回便可。这样就优化了快排的步骤。

4.5事实上,上述办法的复杂度仍然是O(nlogn)

快速排序的partition函数将一个数组分为左右两边,而且咱们能够知道,若是flag值在k位置左边,那么往左找,若是在k位置右边,那么往左找。

这里科普一下经典快排中的一个方法partition,剑指offer书中直接跳过了这部分,让我摸不着头脑。

虽然快排用到了经典的分而治之的思想,可是快排实现的前提仍是在于 partition 函数。正是有了 partition 的存在,才使得能够将整个大问题进行划分,进而分别进行处理。

除了用来进行快速排序,partition 还能够用 O(N) 的平均时间复杂度从无序数组中寻找第K大的值。和快排同样,这里也用到了分而治之的思想。首先用 partition 将数组分为两部分,获得分界点下标 pos,而后分三种状况:

pos == k-1,则找到第 K 大的值,arr[pos];
pos > k-1,则第 K 大的值在左边部分的数组。
pos < k-1,则第 K 大的值在右边部分的数组。
下面给出基于迭代的实现(用来寻找第 K 小的数):


int find_kth_number(vector<int> &arr, int k){
    int begin = 0, end = arr.size();
    assert(k>0 && k<=end);
    int target_num = 0;
    while (begin < end){
        int pos = partition(arr, begin, end);
        if(pos == k-1){
            target_num = arr[pos];
            break;
        }
        else if(pos > k-1){
            end = pos;
        }
        else{
            begin = pos + 1;
        }
    }
    return target_num;
}


该算法的时间复杂度是多少呢?考虑最坏状况下,每次 partition 将数组分为长度为 N-1 和 1 的两部分,而后在长的一边继续寻找第 K 大,此时时间复杂度为 O(N^2 )。不过若是在开始以前将数组进行随机打乱,那么能够尽可能避免最坏状况的出现。而在最好状况下,每次将数组均分为长度相同的两半,运行时间 T(N) = N + T(N/2),时间复杂度是 O(N)。

因此也就是说,本题用这个方法解的话,复杂度只须要O(n),由于第一次交换须要N/2,j接下来的交换的次数愈来愈少,最后加起来就是O(N)了。

5 因为数字出现次数超过长度的一半,也就是平均每两个数字就有一个该数字,但他们不必定连续,因此变量time保存一个数的出现次数,而后变量x表明目前选择的数字,遍历中,若是x与后一位不相等则time–,time=0时x改成后一位,time从新变为1。最终x指向的数字就是出现次数最多的。

举两个例子,好比1,2,3,4,5,6,6,6,6,6,6。明显符合。1,6,2,6,3,6,4,6,5,6,6 遍历到最后获得x=6,以此类推,能够知足要求。

找出前k小的数

  • 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
    */

解析:

1若是容许改变数组,那么则能够继承上一题的思想。,使用快速排序中的partition方法,只须要O(N)的复杂度

2使用堆排序

解析:用前k个数构造一个大小为k的大顶堆,而后遍历余下数字,若是比堆顶大,则跳过,若是比堆顶小,则替换掉堆顶元素,而后执行一次堆排序(即根节点向下调整)。此时的堆顶元素已被替换,

而后遍历完全部元素,堆中的元素就是最小的k个元素了。

若是要求最大的k个元素,则构造小顶堆就能够了。

构造堆的方法是,数组的第N/2号元素到0号元素依次向下调整,此时数组就构成了堆。

实际上咱们可使用现成的集合类,红黑树是一棵搜索树,他是排序的,因此能够获得最大和最小值,那么咱们每次和最小值比较,符合条件就进行替换便可。复杂度是O(nlogn)


public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {

        ArrayList<Integer>arrayList=new ArrayList<>();
        if(input==null || input.length==0 ||k==0 ||k>input.length)return arrayList;

        TreeSet<Integer> treeSet=new TreeSet<>();


        for(int i=0;i<input.length;i++){
            if(treeSet.size()<k){
                treeSet.add(input[i]);
            }

            else {

                if(input[i]<treeSet.last()){
                    treeSet.pollLast();
                    treeSet.add(input[i]);
                }
            }
        }

        for(Integer x:treeSet){
        arrayList.add(x);

        }
        return arrayList;

    }

连续子数组的最大和

**

  • Created by 周杰伦 on 2017/3/23.
  • 题目描述
    HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同窗。
    今天测试组开完会后,他又发话了:在古老的一维模式识别中,经常须要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。
    可是,若是向量中包含负数,是否应该包含某个负数,并指望旁边的正数会弥补它呢?
    例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。
    你会不会被他忽悠住?(子向量的长度至少是1)

解析:笨办法须要O(n2)的复杂度。

1 可是实际上只须要
一次遍历便可解决。经过sum保存当前和,而后若是当前和为正,那么继续日后加,若是当前和为负,则直接丢弃,令当前和等于一个新值。而且每一步都要比较当前和与最大值。

*/

public class 连续数字序列的最大和 {
    public int FindGreatestSumOfSubArray(int[] array) {
       if(array==null || array.length==0)return 0;
        int sum=0;int max=array[0];

        for(int i=0;i<array.length;i++){
           //若是当前和<0,那就不加,直接赋新值
            if(sum<=0){
               sum=array[i];
           }//若是当前和大于零,则继续加。
           else {
               sum+=array[i];
           }
           if(max<sum){
            max=sum;
           }

        }
        return max;
    }

2 本题也可使用DP解法

DP数组表明以i为结尾元素的连续最大和

DP[i] = arr[i] (DP[i-1] < 0)
= DP[i] + arr[i] (DP[i -1] > 0)

逆序对

/**

  • Created by 周杰伦 on 2017/3/23.

  • 题目描述
    在数组中的两个数字,若是前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
    输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。
    即输出P%1000000007
    */

    解析:本题采用归并排序的框架,只是在归并的时候作出逆序对查找,具体参见下面代码。
    核心点是,在归并两个有序数组时,若是a数组的元素a1比b数组的元素b1大时,说明有mid - i + 1个数都比b1大。i为a1元素的位置。
    这样子咱们就能够统计逆序对的个数了。经典巧妙。!

    public class 逆序对 {
    public double Pairs = 0;
    public int InversePairs(int [] array) {
    if (array.length0 ||arraynull)
    return 0;
    mergesort(array,0,array.length-1);
    Pairs = Pairs + 1000000007;
    return (int) (Pairs % 1000000007);
    }
    public void merge(int []array,int left,int mid,int right){
    //有一点很重要的是,归并分红两部分,其中一段是left到mid,第二段是mid+1到right。
    //不能从0到mid-1,而后mid到right。由于这样左右不均分,会出错。千万注意。
    //mid=(left+right)/2
    if (array.length0 ||arraynull ||left>=right)
    return ;
    int p=left,q=mid+1,k=0;

    int []temp=new int[right-left+1];
    
         while (p<=mid && q<=right){
             if(array[p]>array[q]){
                 temp[k++]=array[q++];
                 //当前半数组中有一个数p比后半个数组中的一个数q大时,因为两个数组
                 //已经分别有序,因此说明p到中间数之间的全部数都比q大。
                 Pairs+=mid-p+1;
             }
             else temp[k++]=array[p++];
         }
    
         while (p<=mid){
             temp[k++]=array[p++];}
         while (q<=right){
             temp[k++]=array[q++];}
    
    
    
         for (int m = 0; m < temp.length; m++)
             array[left + m] = temp[m];
    
     }
    
     public void mergesort(int []arr,int left,int right){
         if (arr.length==0 ||arr==null)
             return ;
         int mid=(right+left)/2;
         if(left<right) {
             mergesort(arr, left, mid);
             mergesort(arr, mid + 1, right);
             merge(arr, left,mid, right);
         }
     }

数字在排序数组中出现的次数

1 顺序扫描

鲁迅说过:看到排序数组要想到二分法!

2 经过二分查找找到数字k第一次出现的位置,即先比较k和中间值,再依次二分,若是中间值等于k而且中间值左边!=k,则是第一个k。
反之能够找到最后一次出现k的位置。而后相减便可。复杂度是logn

和为s的两个整数,和为s的连续正数序列

1 和为s的两个整数,双指针遍历便可

2 和为s的连续正数序列。维护一个范围,start到end表示目前的数字序列。大于S则start++,小于S则start–

n个色子的点数

求n个色子点数之和等于s的几率

1 递归实现

2 循环实现,全部可能值存成一个数组,大小为6n,而后把每一个出现数字次数存入数组,遍历一遍便可获得几率。

扑克牌的顺子

题目描述
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的运气如何。为了方便起见,你能够认为大小王是0。

把扑克牌存到数组中,而且A看做1,J为11,Q为12,K为13,而后进行排序,若是有不连续的数字,不存在顺子,若是都连续,则是顺子

Arrays.sort(arr);
        for (int i = 1 ;i < arr.length;i ++) {
            if (arr[i] == arr[i - 1]) {
                return false;
            }
            if (arr[i] - arr[i - 1] == 1) {
                continue;
            }else if (arr[i] - arr[i - 1]  - 1 <= cnt) {
                cnt -= arr[i] - arr[i - 1] - 1;
            }else {
                return false;
            }
        }
        return true;
    }

数组中重复的数字

在一个长度为n的数组里的全部数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每一个数字重复几回。请找出数组中任意一个重复的数字。 例如,若是输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

数组中重复的数字

在一个长度为n的数组里的全部数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每一个数字重复几回。请找出数组中任意一个重复的数字。 例如,若是输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

解析:通常使用hashmap便可达到O(n)

但剑指offer的解法能够只用O(1)的空间作到。

实际上当每一个数字a和他们所在的位置n相同时,每一个数字只出现一次,但若是n + 1的位置上的数也是a,那么a就是第一个重复出现的数字。

根据这个思路。咱们在循环中当第i个数a与arr[a]相等时,不变,若是不相等,则二者互换,而后开始下一轮遍历。接下来继续交换,若是出现相等的状况,则就是第一个重复出现的数。

举例 2 3 1 0 2 5 3
1 arr[0] = 2 != 0,因此arr[0]与arr[2]作交换,得1 3 2 0 2 5 3
2 arr[0] = 1 != 0,因此arr[0]和arr[1]交换,的3 1 2 0 2 5 3
3 arr[0] = 3 != 0,因此arr[0]和arr[3]交换,得0 1 2 3 2 5 3
4 arr[0]到arr[3]都符合要求,arr[4] = 2 != 4,因此arr[4]和arr[2]交换,发现二者相等,因此他就是第一个重复的数。

public boolean duplicate(int array[],int length,int [] duplication) {
    if ( array==null ) return false;

    // key step
    for( int i=0; i<length; i++ ){
        while( i!=array[i] ){
            if ( array[i] == array[array[i]] ) {
                duplication[0] = array[i];
                return true;
            }

            int temp = array[i];
            array[i] = array[array[i]];
            array[array[i]] = temp;
        }
    }

    return false;
}

从上述例子能够看到,一个数最多被交换两次。因此复杂度为O(N)

构建乘积数组

题目描述
给定一个数组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]。不能使用除法。

不能用除法,那么就两个循环,一个从0乘到i - 1,一个从i + 1乘到n-1

数据流的中位数

题目描述
如何获得一个数据流中的中位数?若是从数据流中读出奇数个数值,那么中位数就是全部数值排序以后位于中间的数值。若是从数据流中读出偶数个数值,那么中位数就是全部数值排序以后中间两个数的平均值。

解析,与字符流第一个不重复的字符相似,每次添加数字都要输出一次结果。

public class Solution {

    static ArrayList<Integer> list = new ArrayList<>();
    public static void Insert(Integer num) {
        list.add(num);
        Collections.sort(list);
    }

    public static Double GetMedian() {
        if (list.size() % 2 == 0) {
            int l = list.get(list.size()/2);
            int r = list.get(list.size()/2 - 1);
            return (l + r)/2.0;
        }
        else {
            return list.get(list.size()/2)/1.0;
        }
    }


}

滑动窗口中的最大值

给定一个数组和滑动窗口的大小,找出全部滑动窗口里数值的最大值。例如,若是输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有如下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

解析:
1 保持窗口为3进行右移,每次计算出一个最大值便可。

2 使用两个栈实现一个队列,复杂度O(N),使用两个栈实现最大值栈,复杂度O(1)。二者结合能够完成本题。可是太麻烦了。

3 使用双端队列解决该问题。

import java.util.*;
/**
用一个双端队列,队列第一个位置(队头)保存当前窗口的最大值,当窗口滑动一次
1.判断当前最大值是否过时(若是最大值所在的下标已经不在窗口范围内,则过时)
2.对于一个新加入的值,首先必定要先放入队列,即便他比队头元素小,由于队头元素可能过时。
3.新增长的值从队尾开始比较,把全部比他小的值丢掉(由于队列只存最大值,因此以前比他小的能够丢掉)

*/
public class Solution {
   public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        ArrayList<Integer> res = new ArrayList<>();
        if(size == 0) return res;
        int begin; 
        ArrayDeque<Integer> q = new ArrayDeque<>();
        for(int i = 0; i < num.length; i++){
            begin = i - size + 1;
            if(q.isEmpty())
                q.add(i);
            else if(begin > q.peekFirst())
                q.pollFirst();
         
            while((!q.isEmpty()) && num[q.peekLast()] <= num[i])
                q.pollLast();
            q.add(i);  
            if(begin >= 0)
                res.add(num[q.peekFirst()]);
        }
        return res;
    }
}

字符串

字符串的排列

输入一个字符串,按字典序打印出该字符串中字符的全部排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的全部字符串abc,acb,bac,bca,cab和cba。

解析:这是一个全排列问题,也就是N个不一样的数排成全部不一样的序列,只不过把数换成了字符串。
全排列的过程就是,第一个元素与后续的某个元素交换,而后第二个元素也这么作,直到最后一个元素为之,过程是一个递归的过程,也是一个dfs的过程。

注意元素也要和本身作一次交换,要否则会漏掉本身做为头部的状况。

而后再进行一次字典序的排序便可。

public static ArrayList<String> Permutation(String str) {
        char []arr = str.toCharArray();
        List<char []> list = new ArrayList<>();
        all(arr, 0, arr.length - 1, list);
        Collections.sort(list, (o1, o2) -> String.valueOf(o1).compareTo(String.valueOf(o2)));
        ArrayList<String> res = new ArrayList<>();
        for (char[] c : list) {
            if (!res.contains(String.valueOf(c)))
            res.add(String.valueOf(c));
        }
        return res;
    }

    //注意要换完为之,由于每换一次能够去掉头部一个数字,这样能够避免重复
    public static void all(char []arr, int cur, int end, List<char[]> list) {
        if (cur == end) {
//            System.out.println(Arrays.toString(arr));
            list.add(Arrays.copyOf(arr, arr.length));
        }
        for (int i = cur;i <= end;i ++) {
            //这里的交换包括跟本身换,因此只有一轮换完才能肯定一个结果
            swap(arr, cur, i);
            all(arr, cur + 1, end, list);
            swap(arr, cur, i);
        }
    }
    public static void swap(char []arr, int i, int j) {
        if (i > arr.length || j > arr.length || i >= j)return;
        char temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

替换空格

/**
 * Created by 周杰伦 on 2018/2/25.
 * 请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则通过替换以后的字符串为We%20Are%20Happy。
 */
 
 解析:若是单纯地按顺序替换空格,每次替换完还要将数组扩容,再右移,这部操做的时间复杂度就是O(2*N)=O(N),因此总的复杂度是O(n^2),因此应该采起先扩容的办法,统计出空格数,而后扩容,接下来按顺序添加字符,遇到空格直接改为添加%20便可,这样避免了右移操做和屡次扩容,复杂度是O(N)
 
 
public class 替换空格 {
    public static String replaceSpace(StringBuffer str) {
        int newlen = 0;
        for(int i = 0; i < str.length(); i++) {
            if(str.charAt(i) == ' ') {
                newlen = newlen + 3;
            }
            else {
                newlen ++;
            }
        }
        char []newstr = new char[newlen];
        int j = 0;
        for(int i = 0 ; i < str.length(); i++) {
            if (str.charAt(i) == ' ') {
                newstr[j++] = '%';
                newstr[j++] = '2';
                newstr[j++] = '0';
            }else {
                newstr[j++] = str.charAt(i);
            }
        }
        return String.valueOf(newstr);
    }

第一次只出现一次的字符

哈希表可解

翻转单词顺序和左旋转字符串

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

这个解法很经典,先把每一个单词逆序,再把整个字符串逆序,结果就是把每一个单词都进行了翻转。

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

字符串循环左移N位的处理方法也很经典,先把前N位逆序,再把剩余字符串逆序,最后总体逆序。

abcXYZdef -> cbafedZYX -> XYZdefabc

把字符串转换为整数

题目描述
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0

解析:首先须要判断正负号,而后判断每一位是不是数字,而后判断是否溢出,判断溢出能够经过加完第n位的和与未加第n位的和进行比较。最后能够得出结果。因此须要3-4步判断。

表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 可是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。

不得不说这种题型太恶心了,就是须要一直判断边界条件

参考一个答案。比较完整

    bool isNumeric(char* str) {
        // 标记符号、小数点、e是否出现过
        bool sign = false, decimal = false, hasE = false;
        for (int i = 0; i < strlen(str); i++) {
            if (str[i] == 'e' || str[i] == 'E') {
                if (i == strlen(str)-1) return false; // e后面必定要接数字
                if (hasE) return false;  // 不能同时存在两个e
                hasE = true;
            } else if (str[i] == '+' || str[i] == '-') {
                // 第二次出现+-符号,则必须紧接在e以后
                if (sign && str[i-1] != 'e' && str[i-1] != 'E') return false;
                // 第一次出现+-符号,且不是在字符串开头,则也必须紧接在e以后
                if (!sign && i > 0 && str[i-1] != 'e' && str[i-1] != 'E') return false;
                sign = true;
            } else if (str[i] == '.') {
              // e后面不能接小数点,小数点不能出现两次
                if (hasE || decimal) return false;
                decimal = true;
            } else if (str[i] < '0' || str[i] > '9') // 不合法字符
                return false;
        }
        return true;
    }

字符流中第一个不重复的字符

题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

本题主要要注意的是流。也就是说每次输入一个字符就要作一次判断。好比输入aaaabbbcd,输出就是a###b##cd

StringBuilder sb = new StringBuilder();
    int []map = new int[256];
    public  void Insert(char ch)
    {
        sb.append(ch);
        if (map[ch] == 0) {
            map[ch] = 1;
        }else {
            map[ch] ++;
        }
        System.out.println(FirstAppearingOnce());
        
    }
    //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        for (int i = 0;i < sb.length();i ++) {
            if (map[sb.charAt(i)] == 1) {
                return sb.charAt(i);
            }
        }
        return '#';
    }

链表

从尾到头打印链表

考查递归,递归可使输出的顺序倒置

public static void printReverse(Node node) {
        if (node.next != null) {
            printReverse(node.next);
        }
        System.out.print(node.val + " ");
}

链表倒数第k个节点

使用两个指针,一个先走k步。而后一块儿走便可。

反转链表

老生常谈,可是容易写错。

public ListNode ReverseList(ListNode head) {
        if(head==null || head.next==null)return head;
        ListNode pre,next;
        pre=null;
        next=null;
        while(head!=null){
            //保存下一个结点
            next=head.next;
            //链接下一个结点
            head.next=pre;
            pre=head;
            head=next;
        }
        return pre;
    }
}

合并两个排序链表

与归并排序的合并相似

复杂链表的复制

题目描述
输入一个复杂链表(每一个节点中有节点值,以及两个指针,一个指向下一个节点,另外一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,不然判题程序会直接返回空)

这题比较恶心。

解析:

1 直接复制链表,而后再去复制特殊指针,复杂度是O(n2)

2 使用hash表保存特殊指针的映射关系,第二步简化操做,复杂度是O(n)

3 复制每一个节点而且连成一个大链表A-A’-B-B’,而后从头至尾判断特殊指针,若是有特殊指针,则让后续节点的特殊指针指向原节点特殊指针指向的节点的后置节点,晕了吧,其实就是原来是A指向B,如今是A’指向B‘。

最后咱们根据奇偶序号把链表拆开,复杂度是O(N)且不用额外空间。

两个链表的第一个公共节点

1 逆置链表,反向找第一个不一样节点,前一个就是公共节点

2 求长度并相减得n,短的链表先走n步,而后一块儿走便可。

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

这是一个约瑟夫环问题。

1 使用循环链表求解,每次走n步摘取一个节点,而后继续,直到最后一个节点就是剩下的数,空间复杂度为O(n)

2 使用数组来作
public static int LastRemaining_Solution(int n, int m) {
int []arr = new int[n];
for (int i = 0;i < n;i ++) {
arr[i] = i;
}

int cnt = 0;
    int sum = 0;
    for (int i = 0;i < n;i = (i + 1) % n) {
        if (arr[i] == -1) {
            continue;
        }
        cnt ++;
        if (cnt == m) {
            arr[i] = -1;
            cnt = 0;
            sum ++;
        }
        if (sum == n) {
            return i;
        }
    }
    return n - 1;
}

3 使用余数法求解

int LastRemaining_Solution(int n, int m) {
        if (m == 0 || n == 0) {
            return -1;
        }
        ArrayList<Integer> data = new ArrayList<Integer>();
        for (int i = 0; i < n; i++) {
            data.add(i);
        }
        int index = -1;
        while (data.size() > 1) {
//          System.out.println(data);
            index = (index + m) % data.size();
//          System.out.println(data.get(index));
            data.remove(index);
            index--;
        }
        return data.get(0);
    }

链表的环的入口结点

一个链表中包含环,请找出该链表的环的入口结点。

解析:

1 指定两个指针,一个一次走两步,一个一次走一步,而后当两个节点相遇时,这个节点一定在环中。既然这个节点在环中,那么让这个节点走一圈直到与本身相等为之,能够获得环的长度n。

2 获得了环的长度之后,根据数学推导的结果,咱们能够指定两个指针,一个先走n步,而后二者同时走,这样的话,当慢节点到达入口节点时,快节点也转了一圈恰好又到达入口节点,因此也就是他们相等的时候就是入口节点了。

删除链表中重复的节点

题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

保留头结点,而后找到下一个不重复的节点,与他相连,重复的节点直接跳过便可。

#二叉树

二叉搜索树转换为双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能建立任何新的结点,只能调整树中结点指针的指向。

二叉搜索树要转换成有序的双向链表,实际上就是使用中序遍历把节点连入链表中,而且题目要求在原来节点上进行操做,也就是使用左指针和右指针表示链表的前置节点和后置节点。

使用栈实现中序遍历的非递归算法,即可以找出节点的前后关系,依次链接便可。

public TreeNode Convert(TreeNode root) {
        if(root==null)
            return null;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode p = root;
        TreeNode pre = null;// 用于保存中序遍历序列的上一节点
        boolean isFirst = true;
        while(p!=null||!stack.isEmpty()){
            while(p!=null){
                stack.push(p);
                p = p.left;
            }
            p = stack.pop();
            if(isFirst){
                root = p;// 将中序遍历序列中的第一个节点记为root
                pre = root;
                isFirst = false;
            }else{
                pre.right = p;
                p.left = pre;
                pre = p;
            }      
            p = p.right;
        }
        return root;
    }
}

重建二叉树

* 题目描述
 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
 */
 
 解析:首先,头结点必定是先序遍历的首位,而且该节点把中序分为左右子树,根据这个规则,左子树由左边数组来完成,右子树由右边数组来完成,根节点由中间节点来构建,因而便有了以下的递归代码。该题的难点就在于边界的判断。
 
public TreeNode reConstructBinaryTree(int [] pre, int [] in) {
    if(pre.length == 0||in.length == 0){
        return null;
    }
    TreeNode node = new TreeNode(pre[0]);
    for(int i = 0; i < in.length; i++){
        if(pre[0] == in[i]){
            node.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), Arrays.copyOfRange(in, 0, i));//为何不是i和i-1呢,由于要避免出错,中序找的元素须要再用一次。
            node.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length), Arrays.copyOfRange(in, i+1,in.length));
        }
    }
    return node;
}

树的子结构

/**
 * Created by 周杰伦 on 2018/3/27.
 * 输入两棵二叉树A,B,判断B是否是A的子结构。(ps:咱们约定空树不是任意一个树的子结构)
 */

    解析:本题仍是有点难度的,子结构要求节点彻底相同,因此先判断节点是否相同,而后使用先序遍历进行递判断,判断的依据是若是子树为空,则说明节点都找到了,若是原树节点为空,说明找不到对应节点,接着递归地判断该节点的左右子树是否符合要求.

    public class 树的子结构 {

        public boolean HasSubtree(TreeNode root1, TreeNode root2) {
            boolean res = false;
            if (root1 != null && root2 != null) {
                if (root1.val == root2.val) {
                    res = aHasb(root1, root2);
                }
                if (res == false) {
                    res = HasSubtree(root1.left,root2);
                }
                if (res == false) {
                    res = HasSubtree(root1.right,root2);
                }
                return res;
            }
            else return false;
        }
        public boolean aHasb(TreeNode t1, TreeNode t2){
            if (t2 == null) return true;
            if (t1 == null) return false;
            if (t1.val != t2.val) return false;
    
            return aHasb(t1.left,t2.left) && aHasb(t1.right,t2.right);
        }
    }

镜像二叉树

/**
 * Created by 周杰伦 on 2017/3/19.操做给定的二叉树,将其变换为源二叉树的镜像。
 输入描述:
 二叉树的镜像定义:源二叉树
  8
 /  \
 6   10
 / \  / \
 5  7 9 11
 镜像二叉树
   8
 /  \
 10   6
 / \  / \
 11 9 7  5
 */


解析:其实镜像二叉树就是交换全部节点的左右子树,因此使用遍历而且进行交换便可。

/**
 public class TreeNode {
 int val = 0;
 TreeNode left = null;
 TreeNode right = null;

 public TreeNode(int val) {
 this.val = val;

 }

 }
 */
public class 镜像二叉树 {
    public void Mirror(TreeNode root) {
        if(root == null)return;
        if(root.left!=null || root.right!=null)
        {
            TreeNode temp=root.left;
            root.left=root.right;
            root.right=temp;
        }
        Mirror(root.left);
        Mirror(root.right);

    }

树的层次遍历

也就是从上到下打印节点,使用队列便可完成。

二叉树的深度

经典遍历。

判断是否平衡二叉树

判断左右子树的高度差是否 <= 1便可。

二叉搜索树的后序遍历

题目描述
输入一个整数数组,判断该数组是否是某二叉搜索树的后序遍历的结果。若是是则输出Yes,不然输出No。假设输入的数组的任意两个数字都互不相同。

解析:这题其实也很是巧妙。二叉搜索树的特色就是他的左子树都比根节点小,右子树都比跟节点大。然后序遍历的根节点在最后,因此后续遍历的第1到N-1个节点应该是左右子树的节点(不必定左右子树都存在)。

后续遍历的序列是先左子树,再右子树,最后根节点,那么就要求,左半部分比根节点小,右半部分比根节点大,固然,左右部分不必定都存在。

因此,找出根节点后,首先找出左半部分,要求小于根节点,而后找出右半部分,要求大于根节点,若是符合,则递归地判断左右子树到的根节点(本步骤已经将左右部分划分,割据中间节点进行递归),若是不符合,直接返回false。

同理也能够判断前序遍历和中序遍历。

public class 二叉搜索树的后序遍历序列 {
    public static void main(String[] args) {
        int []a = {7,4,6,5};
        System.out.println(VerifySquenceOfBST(a));
    }
    public static boolean VerifySquenceOfBST(int [] sequence) {
        if (sequence == null || sequence.length == 0) {
            return false;
        }
        return isBST(sequence, 0, sequence.length - 1);
    }
    public static boolean isBST(int []arr, int start, int end) {
        if (start >= end) return true;
        int root = arr[end];
        int mid = start;
        for (mid  = start;mid < end && arr[mid] < root;mid ++) {

        }
        for (int i = mid;i < end; i ++) {
            if (arr[i] < root)return false;
        }
        return isBST(arr, start, mid - 1) && isBST(arr, mid, end - 1);
    }
}

二叉树中和为某一值的路径

/**

  • Created by 周杰伦 on 2018/3/29.

  • 题目描述
    输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的全部路径。路径定义为从树的根结点开始往下一直到叶结点所通过的结点造成一条路径。
    */

    解析:因为要求从根节点到达叶子节点,而且要打印出全部路径,因此实际上用到了回溯的思想。

    经过target跟踪当前和,进行先序遍历,当和知足要求时,加入集合,因为有多种结果,因此须要回溯,将访问过的节点弹出访问序列,才能继续访问下一个节点。

    终止条件是和知足要求,而且节点是叶节点,或者已经访问到空节点也会返回。

    public class 二叉树中和为某一值的路径 {
    private ArrayList<ArrayList> listAll = new ArrayList<ArrayList>();
    private ArrayList list = new ArrayList();
    public ArrayList<ArrayList> FindPath(TreeNode root,int target) {
    if(root == null) return listAll;
    list.add(root.val);
    target -= root.val;
    if(target == 0 && root.left == null && root.right == null)
    listAll.add(new ArrayList(list));
    FindPath(root.left, target);
    FindPath(root.right, target);
    list.remove(list.size()-1);
    return listAll;
    }

    static int count = 0;
     static Stack<Integer> path = new Stack<>();
     static Stack<Integer> stack = new Stack<>();
     static ArrayList<ArrayList<Integer>> lists = new ArrayList<>();

    }

二叉树的下一个节点

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点而且返回。注意,树中的结点不只包含左右子结点,同时包含指向父结点的指针。

解析:给出一个比较好懂的解法,中序遍历的结果存在集合中,找到根节点,进行中序遍历,而后找到该节点,下一个节点就是集合后一位

public TreeLinkNode GetNext(TreeLinkNode TreeLinkNode)
    {
        return findNextNode(TreeLinkNode);
    }
    public TreeLinkNode findNextNode(TreeLinkNode anynode) {
        if (anynode == null) return null;
        TreeLinkNode p = anynode;
        while (p.next != null) {
            p = p.next;
        }
        ArrayList<TreeLinkNode> list = inOrderSeq(p);
        for (int i = 0;i < list.size();i ++) {
            if (list.get(i) == anynode) {
                if (i + 1 < list.size()) {
                    return list.get(i + 1);
                }
                else return null;
            }
        }
        return null;

    }
    static ArrayList<TreeLinkNode> list = new ArrayList<>();
    public static ArrayList<TreeLinkNode> inOrderSeq(TreeLinkNode TreeLinkNode) {
        if (TreeLinkNode == null) return null;
        inOrderSeq(TreeLinkNode.left);
        list.add(TreeLinkNode);
        inOrderSeq(TreeLinkNode.right);
        return list;
    }

对称的二叉树

请实现一个函数,用来判断一颗二叉树是否是对称的。注意,若是一个二叉树同此二叉树的镜像是一样的,定义其为对称的。

解析,以前有一题是二叉树的镜像,递归交换左右子树便可求出镜像,而后递归比较两个树的每个节点,则能够判断是否对称。

boolean isSymmetrical(TreeNode pRoot)
    {
        TreeNode temp = copyTree(pRoot);
        Mirror(pRoot);
        return isSameTree(temp, pRoot);
    }


    void Mirror(TreeNode root) {
        if(root == null)return;
        Mirror(root.left);
        Mirror(root.right);
        if(root.left!=null || root.right!=null)
        {
            TreeNode temp=root.left;
            root.left=root.right;
            root.right=temp;
        }


    }
    boolean isSameTree(TreeNode t1,TreeNode t2){
        if(t1==null && t2==null)return true;
        else if(t1!=null && t2!=null && t1.val==t2.val) {
            boolean left = isSameTree(t1.left, t2.left);
            boolean right = isSameTree(t1.right, t2.right);
            return left && right;
        }
        else return false;
    }

    TreeNode copyTree (TreeNode root) {
        if (root == null) return null;
        TreeNode t = new TreeNode(root.val);
        t.left = copyTree(root.left);
        t.right = copyTree(root.right);
        return t;
    }

把二叉树打印成多行

题目描述
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

解析:1 首先要知道到本题的基础思想,层次遍历。

2 而后是进阶的思想,按行打印二叉树并输出行号,方法是,一个节点last指向当前行的最后一个节点,一个节点nlast指向下一行最后一个节点。使用t表示如今遍历的节点,当t = last时,表示本行结束。此时last = nlast,开始下一行遍历。

同时,当t的左右子树不为空时,令nlast = t的左子树和右子树。每当last 赋值为nlast时,行号加一便可。

按之字形顺序打印二叉树

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其余行以此类推。

解析:1 首先要知道到本题的基础思想,层次遍历。

2 而后是进阶的思想,按行打印二叉树并输出行号,方法是,一个节点last指向当前行的最后一个节点,一个节点nlast指向下一行最后一个节点。使用t表示如今遍历的节点,当t = last时,表示本行结束。此时last = nlast,开始下一行遍历。

同时,当t的左右子树不为空时,令nlast = t的左子树和右子树。每当last 赋值为nlast时,行号加一便可。

3 基于第2步的思想,如今要z字型打印,只需把偶数行逆序便可。因此把每一行的数存起来,而后偶数行逆置便可。

ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        LinkedList<TreeNode> queue = new LinkedList<>();
        TreeNode root = pRoot;
        if(root == null) {
            return new ArrayList<>();
        }
        TreeNode last = root;
        TreeNode nlast = root;
        queue.offer(root);
        ArrayList<Integer> list = new ArrayList<>();
        list.add(root.val);
        ArrayList<Integer> one = new ArrayList<>();
        one.addAll(list);
        ArrayList<ArrayList<Integer>> lists = new ArrayList<>();
        lists.add(one);
        list.clear();

        int row = 1;
        while (!queue.isEmpty()){

            TreeNode t = queue.poll();

            if(t.left != null) {
                queue.offer(t.left);
                list.add(t.left.val);
                nlast = t.left;
            }
            if(t.right != null) {
                queue.offer(t.right);
                list.add(t.right.val);
                nlast = t.right;
            }
            if(t == last) {
                if(!queue.isEmpty()) {
                    last = nlast;
                    row ++;
                    ArrayList<Integer> temp = new ArrayList<>();
                    temp.addAll(list);
                    list.clear();
                    if (row % 2 == 0) {
                        Collections.reverse(temp);
                    }
                    lists.add(temp);

                }
            }
        }

        return lists;
    }

序列化和反序列化二叉树

解析:序列化和反序列化关键是要肯定序列化方式。我么使用字符串来序列化。

用#表明空,用!分隔左右子树。

好比 1
2 3
4 5

使用先序遍历
序列化结果是1!2!4!###3!#5!##

反序列化先让根节点指向第一位字符,而后左子树递归进行链接,右子树

public class Solution {
public int index = -1;
StringBuffer sb = new StringBuffer();

String Serialize(TreeNode root) {
       if(root == null) {
        sb.append("#!") ;
    }
    else {
        sb.append(root.val + "!");
        Serialize(root.left);
        Serialize(root.right);
    }

    return sb.toString();
  }
    TreeNode Deserialize(String str) {
        index ++;
        int len = str.length();
        if(index >= len) {
            return null;
        }
        String[] strr = str.split("!");
        TreeNode node = null;
        if(!strr[index].equals("#")) {
            node = new TreeNode(Integer.valueOf(strr[index]));
            node.left = Deserialize(str);
            node.right = Deserialize(str);
        }
        return node;
  }
}

二叉搜索树的第k个结点

解析:二叉搜索树的中序遍历是有序的,只须要在中序中判断数字是否在第k个位置便可。
若是在左子树中发现了,那么递归返回该节点,若是在右子树出现,也递归返回该节点。注意必需要返回,不然结果会被递归抛弃掉。

TreeNode KthNode(TreeNode pRoot, int k)
    {
        count = 0;
        return inOrderSeq(pRoot, k);
    }
    static int count = 0;
    public TreeNode inOrderSeq(TreeNode treeNode, int k) {
        if (treeNode == null) return null;
        TreeNode left = inOrderSeq(treeNode.left, k);
        if (left != null) return left;
        if (++ count == k) return treeNode;
        TreeNode right = inOrderSeq(treeNode.right, k);
        if (right != null) return right;
        return null;
    }

栈和队列

用两个队列实现栈,用两个栈实现队列。

简单说下思路

1 两个栈实现队列,要求先进先出,入队时节点先进入栈A,若是栈A满而且栈B空则把所有节点压入栈B。

出队时,若是栈B为空,那么直接把栈A节点所有压入栈B,再从栈B出栈,若是栈B不为空,则从栈B出栈。

2 两个队列实现栈,要求后进先出。入栈时,节点先加入队列A,出栈时,若是队列B不为空,则把头结点之后的节点出队并加入到队列B,而后本身出队。

若是出栈时队列B不为空,则把B头结点之后的节点移到队列A,而后出队头结点,以此类推。

包含min函数的栈

/**

  • 设计一个返回最小值的栈

  • 定义栈的数据结构,请在该类型中实现一个可以获得栈最小元素的min函数。

  • Created by 周杰伦 on 2017/3/22.
    */

    解析:这道题的解法也是很是巧妙的。由于每次进栈和出栈都有可能致使最小值发生改变。而咱们要维护的是整个栈的最小值。

    若是单纯使用一个数来保存最小值,会出现一种状况,最小值出栈时,你此时的最小值只能改为栈顶元素,但这个元素不必定时最小值。

    因此须要一个数组来存放最小值,或者是一个栈。

    使用另外一个栈B存放最小值,每次压栈时比较节点值和栈B顶端节点值,若是比它小则压栈,不然不压栈,这样就能够从b的栈顶到栈顶依次访问最小值,次小值。以此类推。

    当最小值节点出栈时,判断栈B顶部的节点和出栈节点是否相同,相同则栈B也出栈。

    这样就能够维护一个最小值的函数了。

    同理,最大值也是这样。

    public class 包含min函数的栈 {
    Stack stack=new Stack<>();
    Stack minstack=new Stack<>();

    public void push(int node) {
         if(stack.isEmpty())
         {
             stack.push(node);
             minstack.push(node);
         }
         else if(node<stack.peek()){
             stack.push(node);
             minstack.push(node);
         }
         else {
             stack.push(node);
         }
     }
    
     public void pop() {
         if(stack.isEmpty())return;
         if(stack.peek()==minstack.peek()){
            stack.pop();
            minstack.pop();
         }
         else {
             stack.pop();
         }
     }
    
     public int top() {
       return stack.peek();
     }
    
     public int min() {
         if(minstack.isEmpty())return 0;
      return minstack.peek();
     }

    }

栈的压入和弹出序列

题目描述
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的全部数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不多是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

解析:本题是比较抽象的,首先,根据入栈出栈的规则,咱们能够创建一个栈A,用于保存压栈序列,而后压入第一个元素,比较出栈序列的第一个元素,若是不相等,继续压栈,直到两者相等,此时栈A元素出栈,而后重复上一步的操做。

若是在每次压栈过程当中,入栈序列已经所有入栈A可是仍是找不到出栈序列的第一个元素时,则说明不是出栈序列。

当栈A的元素所有压入并出栈后,若是出栈序列也出栈完毕,则知足题意。

public static boolean IsPopOrder(int[] pushA, int[] popA) {
        Stack<Integer> stack = new Stack<>();
        int j = 0;
        int i = 0;
        while (i < pushA.length) {
            stack.push(pushA[i]);
            i++;
            while (!stack.empty() && stack.peek() == popA[j]) {
                stack.pop();
                j++;
            }
            if (i == pushA.length) {
                if (!stack.empty()) {
                    return false;
                } else return true;
            }
        }
        return false;
    }

排序和查找

旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,咱们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的全部元素都大于0,若数组大小为0,请返回0。

解析:这题的思路很巧妙,若是直接遍历复杂度为O(N),可是使用二分查找能够加快速度,由于两边的数组都是递增的最小值必定在两边数组的边缘,因而经过二分查找,逐渐缩短左右指针的距离,知道左指针和右指针只差一步,那么右指针所在的数就是最小值了。
复杂度是O(logN)

//这段代码忽略了三者相等的状况
public int minNumberInRotateArray(int [] array) {
        if (array.length == 0) return 0;
        if (array.length == 1) return array[0];
        int min = 0;

        int left = 0, right = array.length - 1;
        //只有左边值大于右边值时,最小值才可能出如今中间
        while (array[left] > array[right]) {
            int mid = (left + right)/2;
            if (right - left == 1) {
                min = array[right];
                break;
            }
            //若是左半部分递增,则最小值在右侧
            if (array[left] < array[mid]) {
                left = mid;
            }
            //若是右半部分递增,则最小值在左侧。
            //因为左边值比右边值大,因此两种状况不会同时发生
            else if (array[right] > array[mid]) {
                right = mid ;
            }
        }
        return array[min];

    }
    

    注意:可是当arr[left] = arr[right] = arr[min]时。三个数都相等没法肯定最小值,此时只能遍历。

递归

斐波那契数列

1递归作法

2记忆搜索,用数组存放使用过的元素。

3DP,本题中dp就是记忆化搜索

青蛙跳台阶

一次跳两步或者跳一步,问一共多少种跳法到达n级,因此和斐波那契数列是同样的。

变态跳台阶

一次跳1到n步,问一共几种跳法,这题是找数字规律的,一共有2^(n-1)种方法

矩形覆盖

和上题同样,也是找规律,答案也是2^(n-1)

位运算

二进制中1的个数

  • Created by 周杰伦 on 2018/6/29.

  • 题目描述

  • 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

    解析:
    1 循环右移数字n,每次判断最低位是否为1,可是可能会致使死循环。

    2 使用数字a = 1和n相与,a每次左移一位,再与n相与获得次低位,最多循环32次,当数字1左移32次也会等于0,因此结束循环。

    3 很是奇葩的作法,把一个整数减去1,再与原整数相与,会把最右边的一个1变成0,因而统计能够完成该操做的次数便可知道有多少1了。

    public class 二进制中1的个数 {
         public static int NumberOf1(int n) {
             int count = 0;
             while (n != 0) {
                 ++count;
                 n = (n - 1) & n;
             }
             return count;
         }
     }

数组中只出现一次的数字

题目描述
一个整型数组里除了一个数字以外,其余的数字都出现了两次。请写程序找出这一个只出现一次的数字。

解析:左神称之为神仙题。

利用位运算的异或操做^。
因为a^a = 0,0^b=b,因此。全部数执行异或操做,结果就是只出现一次的数。

不用加减乘除作加法

解析:不用加减乘,那么只能用二进制了。

两个数a和b,若是不考虑进位,则0 + 1 = 1,1 + 1 = 0,0 + 0 = 0,这就至关于异或操做。
若是考虑进位,则只有1 + 1有进位,因此使用相与左移的方法获得每一位的进位值,再经过异或操做和原来的数相加。当没有进位值的时候,运算结束。
public static int Add(int num1,int num2) {
if( num2 == 0 )return num1;
if( num1 == 0 )return num2;

int temp = num2;
    while(num2!=0) {
        temp = num1 ^num2;
        num2 = (num1 & num2)<<1;
        num1 = temp;
    }
    return num1;
}

回溯和DFS

矩阵中的路径

题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串全部字符的路径。路径能够从矩阵中的任意一个格子开始,每一步能够在矩阵中向左,向右,向上,向下移动一个格子。若是一条路径通过了矩阵中的某一个格子,则以后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,可是矩阵中不包含"abcb"路径,由于字符串的第一个字符b占据了矩阵中的第一行第二个格子以后,路径不能再次进入该格子。

解析:回溯法也就是特殊的dfs,须要找到全部的路径,因此每当到达边界条件或抵达目标时,递归返回,因为须要保存路径中的字母,因此递归返回时须要删除路径最后的节点,来保证路径合法。不过本题只有一个解,因此找到便可返回。

public class 矩阵中的路径 {
    public static void main(String[] args) {
        char[][]arr = {{'a','b','c','e'},{'s','f','c','s'},{'a','d','e','e'}};
        char []str = {'b','c','c','e','d'};
        System.out.println(hasPath(arr, arr.length, arr[0].length, str));
    }
    static int flag = 0;
    public static boolean hasPath(char[][] matrix, int rows, int cols, char[] str)
    {
        int [][]visit = new int[rows][cols];
        StringBuilder sb = new StringBuilder();
        for (int i = 0;i < rows;i ++) {
            for (int j = 0;j < cols;j ++) {
                if (matrix[i][j] == str[0]) {
                    visit[i][j] = 1;
                    sb.append(str[0]);
                    dfs(matrix, i, j, visit, str, 1, sb);
                    visit[i][j] = 0;
                    sb.deleteCharAt(sb.length() - 1);
                }
            }
        }
        return flag == 1;
    }
    public static void dfs(char [][]matrix, int row, int col, int [][]visit, char []str, int cur, StringBuilder sb) {
        if (sb.length() == str.length) {
//            System.out.println(sb.toString());
            flag = 1;
            return;
        }

        int [][]pos = {{1,0},{-1,0},{0,1},{0,-1}};
        for (int i = 0;i < pos.length;i ++) {
            int x = row + pos[i][0];
            int y = col + pos[i][1];
            if (x >= matrix.length || x < 0 || y >= matrix[0].length || y < 0) {
                continue;
            }
            if (visit[x][y] == 0 && matrix[x][y] == str[cur]) {
                sb.append(matrix[x][y]);
                visit[x][y] = 1;
                dfs(matrix, x, y, visit, str, cur + 1, sb);
                sb.deleteCharAt(sb.length() - 1);
                visit[x][y] = 0;
            }
        }
    }

机器人的运动范围

题目描述
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,可是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人可以进入方格(35,37),由于3+5+3+7 = 18。可是,它不能进入方格(35,38),由于3+5+3+8 = 19。请问该机器人可以达到多少个格子?

解析:这是一个可达性问题,使用dfs方法,走到的每一格标记为走过,走到无路可走时就是最终的结果。每次都有四个方向能够选择,因此写四个递归便可。

public class Solution {
    static int count = 0;
    public static int movingCount(int threshold, int rows, int cols)
    {
        count = 0;
        int [][]visit = new int[rows][cols];
        dfs(0, 0, visit, threshold);
        return count;
    }

    public static void dfs(int row, int col, int[][]visit, int k) {
        if (row >= visit.length || row < 0 || col >= visit[0].length || col < 0) {
            return;
        }
        if (sum(row) + sum(col) > k) {
            return;
        }

        if (visit[row][col] == 1){
            return;
        }

        visit[row][col] = 1;
        count ++;
        dfs(row + 1,col,visit, k);
        dfs(row - 1,col,visit, k);
        dfs(row,col + 1,visit, k);
        dfs(row,col - 1,visit, k);

    }

    public static int sum(int num) {
        String s = String.valueOf(num);
        int sum = 0;
        for (int i = 0;i < s.length();i ++) {
            sum += Integer.valueOf(s.substring(i, i + 1));
        }
        return sum;
    }
}
## 微信公众号

我的公众号:程序员黄小斜

微信公众号【程序员黄小斜】新生代青年汇集地,程序员成长充电站。做者黄小斜,职业是阿里程序员,身份是斜杠青年,但愿和更多的程序员交朋友,一块儿进步和成长!专一于分享技术、面试、职场等成长干货,这一次,咱们一块儿出发。

关注公众号后回复“2019”领取我这两年整理的学习资料,涵盖自学编程、求职面试、算法刷题、Java技术学习、计算机基础和考研等8000G资料合集。

技术公众号:Java技术江湖

微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,专一于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!

关注公众号后回复“PDF”便可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖全部Java工程师必知必会的知识点。