剑指offer算法题分析与整理(一)

下面整理一下我在刷剑指offer时,自己做的和网上大神做的各种思路与答案,自己的代码是思路一,保证可以通过,网友的代码提供出处链接。

目录

1、数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
思路:很明显,暴力解答时间复杂度为O(n^2),不可取。一种很巧妙的方法是,利用归并排序,在两段相互比较排序的过程中,统计逆序对。

public class Solution {
    public int InversePairs(int [] array) {
        int N=array.length;
        if(N==1) return 0;
        int mid=(N-1)/2;
        int [] leftArr=new int[mid+1];
        int [] rightArr=new int[N-1-mid];

        for(int i=0;i<=mid;i++) leftArr[i]=array[i];
        for(int i=0;i<=N-2-mid;i++) rightArr[i]=array[i+mid+1];

        int leftNum=InversePairs(leftArr)%1000000007;
        int rightNum=InversePairs(rightArr)%1000000007;
        int i=0,j=0,k=0,num=0;

        while(i<=mid&&j<=N-2-mid){
            if(leftArr[i]<rightArr[j]){
                array[k++]=leftArr[i++]; }
            else{
                array[k++]=rightArr[j++];
                num=(num+mid-i+1)%1000000007;//当发生left大right小时,left大后面的肯定都比该right小要大,统计left里面所有比right小要大的数
            }
        }
        while(i<=mid){array[k++] = leftArr[i++];}
        while(j<=N-2-mid){array[k++]=rightArr[j++];}

        int sumNum=(leftNum+rightNum+num)%1000000007;
        return sumNum;
    }
}

下面贴一下我在写的过程中出错的一段

//这一段是错的
while(i<=mid||j<=N-2-mid){
            if(i==mid+1){array[k++]=rightArr[j++];}
            else if(j==N-1-mid) {
                array[k++] = leftArr[i++];
                if (i - 1 == flag) { } //111
                else { num += N - 1 - mid;}
            }
            else if(leftArr[i]<rightArr[j]){array[k++]=leftArr[i++];}
            else if(leftArr[i]>rightArr[j]){
                array[k++]=rightArr[j++];
                if(i==flag){num++;}//222
                else{ num+=j;}
                flag=i;
            }
 }

分析上面对与错的解答,两者的区别是

  • 前者是在发生左边比右边大时,统计左边所有比右边这个数大的对数
  • 后者是在发生左边比右边大时,统计右边所有比左边这个数小的对数

在归并排序的过程中统计逆序对,对于两段已经排好序的leftArr和rightArr,我们采取的是从两段的头部开始比较,取较小的数填进Array中。既然是取小,就

  • 一定不会发生多大(左)对同小(右)的局面(右边的一小只会对左边的一大,而且按后者的统计方法,可能会发生左边明明有比右边大的,但却没有计数,如【1,2,3】对【0,4,5】2和3对0这两队就没计入,如果会存在多大对同小,也就不会漏了)
  • 但反而会发生一大(左)对多小(右)的局面,按后者的统计方法这时会重复统计左边这一大与右边好多小数之间的对数

因此后者在解决这两个问题的路上疲于奔命,标记2处试图解决上面第二点的重复问题,标记1处试图解决一大(左)对多小(右)时,右边的数比完了,在进行下一次循环往Array填数时会重复计算 一大(左)对右边末位数 这一对。还有上面第一点提到的漏掉的问题,还没改,已经改不下去了···
其实只要转向就不会这么折腾=_=

参考牛客网优质解答,网上还有许多从leftArr和rightArr的尾部开始比较,取较大的数填进Array中(当然是从它的尾部开始填),这时所有的情况与上面分析的相反,就应该采用后者的统计方法了。

2、二维数组中的查找

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路一:我最开始想法是,该数组中的每个数,它的下面和右边的数都比它大,把下面和右边的数当作它的分支,就是一个最小二叉堆(堆中的元素有很多重合的,因为重合的对应数组中的同一个元素),采用深度优先遍历。

public class Solution {
    public boolean Find(int target, int [][] array) {
        int row = array.length;//二维数组的行数
        if (row == 0) return false;
        //if (array == null) return false;//!!!!判断数组是否为空,不能这样
        int columnMax = 0;
        for (int m = 0; m < row; m++) {
            if (array[m].length > columnMax) {
            columnMax = array[m].length;}//m行对应的列数
        }

        int[][] flag=new int[row][columnMax];//避免把同一个元素比多次
        int[] arr = new int[2 * row * columnMax];//类似于栈的功能,存数值的坐标
        int i, j, k = 0;
        arr[2 * k] = 0;
        arr[2 * k + 1] = 0;
        while (k >= 0) {//当栈内还有元素时
            i = arr[2 * k];
            j = arr[2 * k + 1];
            k--;//弹出栈顶的坐标,准备比较
            flag[i][j]=1;//每个数比较之后就被标记
            if (array[i][j] < target) {//当前数比目标小,把它的分支压入栈中
                if (j + 1 < array[i].length&&flag[i][j+1]==0) {//判断一下右边的分支越界没有
                    k++;
                    arr[2 * k] = i;
                    arr[2 * k + 1] = j + 1;
                }
                if (i + 1 < row&&flag[i+1][j]==0) {//判断下边的分支越界没有
                    k++;
                    arr[2 * k] = i + 1;
                    arr[2 * k + 1] = j;
                }

            } else if (array[i][j] > target) {//当前数比目标大,它的分支就没必要再参与比较了
                for(int m=i;m<row;m++){
                    for(int n=j;n<array[m].length;n++){
                        flag[m][n]=1;
                    }
                }
            } else return true;
        }
        return false;
    }
}

思路二:该二维数组的性质决定了,一、它的左上右下对角线是递增的;二、对于数组中的一个数,它的左上角矩阵都比它小,右下角矩阵都比它大。可以先遍历对角线(不是方针的可以先按对角线补齐,会有外延对角线段),找到对角线中关于目标值的左右两个临界点(没有小点,说明目标值小于矩阵所有值,没有大点就把外延线第一个点看作大点),小点的左上角矩阵全排除,大点的右下角矩阵全排除,剩下的两个矩阵重复这个操作。初步想法,没代码实现。

思路三:对每行或每列用二分查找(总感觉怪怪的,因为只用到了二维数组的一个特性);那如果对行与列都用二分查找呢,那一次只能排除矩阵的四分之一,感觉效率不高。

思路四:参考牛客网@徘徊的路人甲,最简单高效的神操作,我的思维局限在从左上角开始了。左下和右上开始,豁然开朗!

public class Solution {
    public boolean Find(int target, int [][] array) {
        int rows = array.length;
        int cols = array[0].length;
        int i=rows-1,j=0;
        while(i>=0 && j<cols){
            if(target<array[i][j])
                i--;
            else if(target>array[i][j])
                j++;
            else
                return true;
        }
        return false;
    }
}

3、顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
思路一:判断矩阵是不是一列或者一行,写出一列、一行、一圈的打印操作,再判断打印完没有,没有就继续从里面重复这些操作。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        ArrayList<Integer> A=new ArrayList();
        int rows=matrix.length;
        int cols=matrix[0].length;
        int colsM=cols;
        int rowsM=rows;

        int i=0,j=-1;

        while(A.size()<rowsM*colsM) {//判断有没有往里面塞完
            if(rows==1) {//一行
                for (int m = 0; m < cols; m++) {
                    j++;
                    A.add(matrix[i][j]);
                }
            }
            else if(cols==1){//一列
                j++;
                for (int m = 0; m < rows; m++) {
                    A.add(matrix[i][j]);
                    i++;
                }
            }
            else {//一圈
                for (int m = 0; m < cols; m++) {
                    j++;
                    A.add(matrix[i][j]);
                }
                for (int m = 0; m < rows - 1; m++) {
                    i++;
                    A.add(matrix[i][j]);
                }
                for (int m = 0; m < cols - 1; m++) {
                    j--;
                    A.add(matrix[i][j]);
                }
                for (int m = 0; m < rows - 2; m++) {
                    i--;
                    A.add(matrix[i][j]);
                }
                rows -= 2;
                cols -= 2;
            }
        }
        return A;
    }
}

以下内容有一部分来自牛客网解答,其实while循环里面还可以再简化为

                for (int m = 0; m < cols; m++) {
                    j++;
                    A.add(matrix[i][j]);
                }
                if(rows==1)break;//不管是不是只有一行,第一个for循环都是一样的
                for (int m = 0; m < rows - 1; m++) {
                    i++;
                    A.add(matrix[i][j]);
                }
                if(cols==1)break;//不管是不是只有一列,前两个for循环都是一样的
                for (int m = 0; m < cols - 1; m++) {
                    j--;
                    A.add(matrix[i][j]);
                }
                for (int m = 0; m < rows - 2; m++) {
                    i--;
                    A.add(matrix[i][j]);
                }

同样是这种思路,上面的 ij 在变化过程中我不直接给它们赋值,而是根据走多少步让它们转弯,让它们在前一个的基础上递增或递减;下面的另一种实现方式,它相当于将一张纸用左上和右下的两个钉子订起来,钉子决定了遍历范围。

 int top = 0, left = 0, right = col-1, bottom = row-1;
 while(top <= bottom && left<= right){
     for(int i = left; i <= right; ++i)  A.add(matrix[top][i]);  
     for(int i = top+1; i <= bottom; ++i)  A.add(matrix[i][right]);
      //不是单行或单列才有下面的   
     for(int i = right-1; i >= left && top < bottom; --i){
         A.add(matrix[bottom][i]);
     }
     for(int i = bottom-1; i > top && right > left; --i){
     A.add(matrix[i][left]);
     }
     ++top; ++left; --right; --bottom;
 }

在判断什么时候数才打印完了,还可以用层数判断,虽然我觉得上面的更容易理解。

    int layers = (Math.min(row,col)-1)/2+1;//这个是层数
    for(int i=0;i<layers;i++){
       for(int k = i;k<col-i;k++) A.add(array[i][k]);
       for(int j=i+1;j<row-i;j++) A.add(array[j][col-i-1]);
       for(int k=col-i-2;(k>=i)&&(row-i-1!=i);k--) A.add(array[row-i-1][k]);
       for(int j=row-i-2;(j>i)&&(col-i-1!=i);j--) A.add(array[j][i]);
    }

这几种实现方式都是同一种思想。

思路二:打印完矩阵第一行后,将矩阵向左旋转90度,重复该操作。但是这样每打一行就要旋转,很花时间,矩阵大了更严重。

  public ArrayList<Integer> printMatrix(int[][] matrix) {
            ArrayList<Integer> A = new ArrayList();
            int row = matrix.length;
            while (row != 0) {
                for (int i = 0; i < matrix[0].length; i++) {
                    A.add(matrix[0][i]);
                }
                if (row == 1)
                    break;
                matrix = turn(matrix);
                row = matrix.length;
            }
            return A;
        }

        private int[][] turn(int[][] matrix) {
            int col = matrix[0].length;
            int row = matrix.length;
            int[][] newMatrix = new int[col][row - 1];
            for (int j = col - 1; j >= 0; j--) {
                for (int i = 1; i < row; i++) {
                    newMatrix[col - 1 - j][i - 1] = matrix[i][j];
                }
            }
            return newMatrix;
        }

思路三:不用管每次横着或者竖着要走几步,每走一步就看看下一步还能不能走,不能走就及时修正方向,直到遍历完整个数组。不过这种方法每走一步就要判断一下,牛客网显示超时=_=!

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        ArrayList<Integer> A=new ArrayList();
        int rows=matrix.length;
        int cols=matrix[0].length;
        int i=0,j=0;
        int [][] flag=new int[rows][cols];//为1就是已经走过了,不能再走了
        while(A.size()<rows*cols){
            //从左到右走
            while(i>=0&&i<rows&&j>=0&&j<cols&&flag[i][j]==0){ A.add(matrix[i][j]);flag[i][j]=1;j++; }
            j--;//走出头了,回退
            i++;//换向
            while(i>=0&&i<rows&&j>=0&&j<cols&&flag[i][j]==0){ A.add(matrix[i][j]);flag[i][j]=1;i++; }
            i--;
            j--;
            while(i>=0&&i<rows&&j>=0&&j<cols&&flag[i][j]==0){ A.add(matrix[i][j]);flag[i][j]=1;j--; }
            j++;
            i--;
            while(i>=0&&i<rows&&j>=0&&j<cols&&flag[i][j]==0){ A.add(matrix[i][j]);flag[i][j]=1;i--; }
            i++;
            j++;
        }
        return A;
    }
}

同样的思路,不一样的实现方法。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        ArrayList<Integer> A=new ArrayList();
        int rows=matrix.length;
        int cols=matrix[0].length;
        int [][] direction={{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
        int i=0,j=0,d=0;
        int [][] flag=new int[rows][cols];

        while(A.size()<rows*cols){

            A.add(matrix[i][j]);
            flag[i][j]=1;
            //打印完,标记完后,看看下一步能不能走,如果不能走就要换方向了
            if(!(i+direction[d][0]>=0&&i+direction[d][0]<rows&&j+direction[d][1]>=0&&j+direction[d][1]<cols&&flag[i+direction[d][0]][j+direction[d][1]]==0)){
                d++;d=d%4;
            }
            i+=direction[d][0];//按照给定的方向走
            j+=direction[d][1];
        }
        return A;
    }
}

4、翻转单词顺序

例如:将“I am a student.”变为“student. a am I”。单词之间可能有多个空格,句子头和尾可能有多个空格。
思路一:从尾到头遍历,遇到空格段,取出来把它填入新字符串,遇到单词段,也取出来填入新的字符串,最后返回该新字符串。二是从头到尾遍历,遇到单词段从右边开始填入新字符串。

public class Solution {
    public String ReverseSentence(String str) {
        StringBuffer A=new StringBuffer();
        int len=str.length();
        if(len==0){return str;}
        int r=len-1,l=r;
        while(l>=0){
           //下面的循环判断两个条件前后不能换,不然java.lang.StringIndexOutOfBoundsException: String index out of range: -1
           while(l>=0&&str.charAt(l)==' '){l--;}//找空格段
           A.append(str.substring(l+1,r+1));//从l+1到r的字符串,如果l=r那么就为空
           r=l;
           while(l>=0&&str.charAt(l)!=' '){l--;}//找单词段
           A.append(str.substring(l+1,r+1));
           r=l;
       }
       return A.toString();
   }
}

以下内容来自牛客网,下面是C++从前到后遍历的。而Java里面String是不可变的,所以这样频繁的用+连接生成新的字符串在Java里不好。

class Solution {
public:
    string ReverseSentence(string str) {
        string res = "", tmp = "";
        for(unsigned int i = 0; i < str.size(); ++i){
            if(str[i] == ' ') res = " " + tmp + res, tmp = "";
            else tmp += str[i];
        }
        if(tmp.size()) res = tmp + res;
        return res;
    }
}; 

思路二:两次翻转,一次全部翻转,一次只翻转单词。

public class Solution {
    public String ReverseSentence(String str) {
        char[] chars = str.toCharArray();
        reverse(chars,0,chars.length - 1);
        int blank = -1;//用一个全局变量来记录段的起点终点
        for(int i = 0;i < chars.length;i++){
            if(chars[i] == ' '){ //对于单词段,可以取出并进行翻转;对于空格段,仍然更新blank,但不做任何操作
                int nextBlank = i;
                reverse(chars,blank + 1,nextBlank - 1);
                blank = nextBlank;
            }
        }
        if(chars[chars.length-1]!=' '){//当最后一个单词后面没有空格,需要在这里将其翻转
             reverse(chars,blank + 1,chars.length - 1);
        }
        return new String(chars);     
    }

    public void reverse(char[] chars,int low,int high){
        while(low < high){
            char temp = chars[low];
            chars[low] = chars[high];
            chars[high] = temp;
            low++;
            high--;
        }
    }
}

另一种C++版本如下

    void ReverseWord(string &str, int s, int e){
        while(s < e){swap(str[s++], str[e--]);}
    }

    string ReverseSentence(string str) {
        ReverseWord(str, 0, str.size() - 1); //先整体翻转
        int s = 0, e = 0;
        int i = 0;
        while(i < str.size()){
            while(i < str.size() && str[i] == ' '){i++;}//空格跳过
            e = s = i; //记录单词的第一个字符的位置
            while(i < str.size() && str[i] != ' '){//找单词后的第一个空格位置
                i++;
                e++;
            }
            ReverseWord(str, s, e - 1); //局部翻转
        }
        return str;
    }

5、删除链表中重复的结点

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
思路一:从头节点开始遍历,维护一个前点toa,一个现点a,一个后点b。当现点a与其next后点b的值相同时,后点往后移直到与现点的值不同,将前点的next指向后点,然后更新现点,将前点的next继续赋给现点,始终保持前点的next是现点;当现点a与后点b不同时,整体更新,将现点a赋给前点toa,后点b赋给现点a。

/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {//有了有参的构造方法,就不能调用默认无参构造方法了,除非自己写个无参的
        this.val = val;
    }
}
*/

public class Solution {
    public ListNode deleteDuplication(ListNode pHead)//注意,传入的是链表的第一个节点,不是空头节点
    {
       ListNode toa=new ListNode(0);//这个0没意义,随便都可以
       toa.next=pHead;
       ListNode star=toa;//star是不变的空头节点
       ListNode a=toa.next;
       while(a!=null){
            ListNode b=a.next; 

            if(b!=null&&a.val==b.val){     //if里面是改变链接关系
                while(b!=null&&a.val==b.val){
                    b=b.next;
                 }
            toa.next=b;
            a=toa.next;
            }

            else{                          //else里面是向后推进
                toa=a;
                a=b;
            }
       }
       return star.next;//返回也返回新链表的第一个节点
    }
}

参考牛客网@Kevin_Xiu的答案的思想跟我的一样,只是它更简洁,少用了一个节点,它不需要节点b。

public static ListNode deleteDuplication(ListNode pHead) {
        ListNode first = new ListNode(-1);//first就是star
        first.next = pHead;
        ListNode p = pHead;   //p是现点a,  p.next是后点b
        ListNode last = first;//last是toa

        while (p != null && p.next != null) {//这里加了第二个判断条件,现点p是最后一个点时,上面我的虽然会进入循坏,但不会改变原有的链接关系,所以这里不进循环也可以
            if (p.val == p.next.val) {
                int val = p.val;
                while (p!= null&&p.val == val)
                     p = p.next;
                last.next = p;
            } else {
                last = p;
                p = p.next;
            }
        }
        return first.next;
    }

思路二:前面的解答最开始我以为传入的是链表的空头节点,返回的也是空头节点,结果运行1,1,1,4发现最开始的1删不掉,所以传入的是第一个节点。那么就可以用递归的方法,每次递归,删除目前向后的第一段连续的节点。保证下一次递归的第一个结点与之后的不重复。
来自牛客网@program

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        if (pHead == null || pHead.next == null) { // 只有0个或1个结点,则返回
            return pHead;                          //递归一定要有这个,不然无穷递归了
        }

        if (pHead.val == pHead.next.val) { // 当前结点是重复结点
            ListNode pNode = pHead.next;

            while (pNode != null && pNode.val == pHead.val) {
                // 跳过值与当前结点相同的全部结点,找到第一个与当前结点不同的结点
                pNode = pNode.next;
            }
            return deleteDuplication(pNode); // 从第一个与当前结点不同的结点开始递归

            else { // 当前结点不是重复结点
            pHead.next = deleteDuplication(pHead.next); // 保留当前结点,从下一个结点开始递归
            return pHead;
        }
    }
}

6、正则表达式匹配

请实现一个函数用来匹配包括’.’和’*’的正则表达式。模式中的字符’.’表示任意一个字符,而’*’表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串”aaa”与模式”a.a”和”ab*ac*a”匹配,但是与”aa.a”和”ab*a”均不匹配。

思路:从模式串开始遍历,无非是四种情况:字母、点、字母*、点*。

  • 前两种好办,与字符串一一对着比,字母不同直接false,点都不用比直接算它相等。
  • 模式串中的字母*,如果字符串中对应的字母与该字母不同,那就当这个字母*不存在吧,模式串的遍历向后跳两步;如果字符串中对应的字母与该字母相同,那么就分别递归比较模式串字母*后面的模式串与字符串的匹配情况,只要有一种匹配成功,就算整个都成功了。
  • 模式串中的点*,可以看成是任意字母的任意个数(可以是0个)的组合,因此将点*后面的视作模式串,将其分别与字符串递归匹配,只要有一种匹配成功,就算整个都成功了。

上面提到的字符串怎样构造,在代码中详细解释。
还有,上面用递归,一个设定是:字符串与模式串相匹配,当模式串为空时,如若字符串不为空,则结果为false,若字符串也为空,则结果为true。

public class Solution {
    public boolean match(char[] str, char[] pattern){   
        if(pattern.length==0&&str.length!=0)return false;
        if(pattern.length==0&&str.length==0)return true;

        int j=0;//字符串的遍历指标

        for(int i=0;i<pattern.length;i++){//以模式串为参考系,一步步遍历模式串
            //这是 字母* 的情况
            if(pattern[i]!='.'&&pattern[i]!='*'&&i+1<pattern.length&&pattern[i+1]=='*') {
               //如果 字母*(a*) 的这个字母与 字符串 中对应的字母(a)相等(这时字符串还没遍历完)
               if(j >= 0 && j < str.length &&str[j] == pattern[i]) {
                   int i1=i,j1=j;
                   //生成新的模式串(就是字母*后面的)
                   char[] pattern1 = new char[pattern.length - i1-2];
                   for (int m = 0; m < pattern1.length; m++) {
                       pattern1[m] = pattern[i1+2];
                       i1++;
                   }
                   //找字符串中,连着有几个重复的a
                   while (j1 >= 0 && j1< str.length && str[j1] == pattern[i]){j1++;}
                   //如果有x个重复的a,下面就循环x+1次,生成x+1个新字符串,分别与新模式串递归匹配
                   //这x+1次分别代表 a*等价于字符串中的 空、一个a、两个a、···、x个a
                   for(int n=j;n<=j1;n++) {
                       int j2=n;
                       char[] str1 = new char[str.length - n];
                       for (int m = 0; m < str1.length; m++) {//生成新字符串
                           str1[m] = str[j2];
                           j2++;
                       }

                       if (match(str1, pattern1)) {//递归匹配,有一次成了,那就成了
                           return true;
                       }
                   }
                   return false;//没一次成,那就返回false

               }
               else{ i++;}//a*这个a和字符串中的b根本就不同,那就忽略a*吧||或者字符串已经遍历完了
            }
            //这是 .* 的情况
            else if(pattern[i]=='.'&&i+1<pattern.length&&pattern[i+1]=='*'){
                char[] pattern2 = new char[pattern.length - i-2];//生成新模式串
                for (int m = 0; m < pattern2.length; m++) {
                    pattern2[m] = pattern[i+2];
                    i++;
                }

                for(int n=j;n<=str.length;n++) {//生成新字符串
                    char[] str2 = new char[str.length - n];
                    int j3=n;
                    for (int m = 0; m < str2.length; m++) {
                        str2[m] = str[j3];
                        j3++;
                    }

                    if (match(str2, pattern2)) {//递归匹配
                        return true;
                    }
                }

                return false;//同理
            }
            //这是 字母 的情况
            else if(pattern[i]!='.'){
                if(j>=0&&j<str.length&&pattern[i]!=str[j]){return false;}//字符串还没遍历完时,不匹配直接false
                else{
                    j++;//匹配就看 模式串的下一个 与 字符串的下一个||或者j已经超了字符串的长度了,那这时很明显应该返回false啊,别急,最后会处理
                }
            }
            //这是 . 的情况
            else if(pattern[i]=='.'){
                j++;//都不用比了,默认是匹配的,直接看 模式串的下一个 与 字符串的下一个
            }

        }
        //其实前两个if里面的情况,递归了,都会在它里面给出结果;如果没发生递归,就看模式串遍历完后,字符串也相应的确认到了str.length,如果是,说明匹配成功,反之不成功
        //但是这样,就是要等到pattern完全遍历完再给结果,可能str很短,pattern很长,str早就超出边界范围了,所以早就可以确定是false了,结果像你这样搞,j还在一直加加加
        //但是这种写法(外面一个大for),就是要等整个大循环完了才好得出结果,当然,刚才说的上一点可以改进,如果str先超出边界,可以直接返回false。但下面的这个最终判断还是要有
        if(j==str.length){return true;}
        else{return false;}
    }
}

上面我的解答还有一些可以改进的地方:

  • 其实在前两个if里面没必要构造那么多新的数组,既然是递归,其实两个就够了,我写了那么多让他们分别递归匹配,其实在递归过程中有很多重复的判断
  • 前两个if可以合并在一起,即.*的情况可视为a*中a与str中a匹配的情况
public class Solution {
    public boolean match(char[] str, char[] pattern){   
        if(pattern.length==0&&str.length!=0)return false;
        if(pattern.length==0&&str.length==0)return true;

        int j=0;

        for(int i=0;i<pattern.length;i++){
            //a*与.*的情况
            if(i+1<pattern.length&&pattern[i+1]=='*') {
               //j没越界时,a*与a相等,或.*
               if((j >= 0 && j < str.length &&str[j] == pattern[i])||(j >= 0 && j < str.length &&pattern[i]=='.')) {
                   //构造新pattern
                   int i1=i+2;
                   char[] patternA2 = new char[pattern.length - i1];
                   for (int m = 0; m < patternA2.length; m++) {
                       patternA2[m] = pattern[i1];
                       i1++;
                   }
                   //此时往后截断的pattern
                   int i2=i;
                   char[] patternNow=new char[pattern.length - i2];
                   for (int m = 0; m < patternNow.length; m++) {
                       patternNow[m] = pattern[i2];
                       i2++;
                   }
                   //构造新str
                   int j2 = j + 1;
                   char[] strA1 = new char[str.length - j2];
                   for (int m = 0; m < strA1.length; m++) {
                        strA1[m] = str[j2];
                        j2++;
                   }
                   //此时往后截断的str
                   int j1=j;
                   char[] strNow=new char[str.length - j1];
                   for (int m = 0; m < strNow.length; m++) {
                       strNow[m] = str[j1];
                       j1++;
                   }
                      /*
                   if (match(strNow, patternA2)) {//视作a*或.*没等价于str中任何字符
                           return true;
                   }
                   if (match(strA1, patternNow)) {//视作a*或.*等价于str中的a
                       return true;
                   }
                      */
                   //这样写表达更简洁
                   return match(strNow, patternA2)||match(strA1, patternNow);

               }
               else{ i++;}//j越界或a*与b不等
            }


            else if(pattern[i]!='.'){
                //新增j越界就直接false
                if((j>=0&&j<str.length&&pattern[i]!=str[j])||j>=str.length){return false;}
                else{
                    j++;
                }
            }

            else if(pattern[i]=='.'){
                if(j>=str.length){return false;}//新增j越界就直接false
                else{
                j++;}
            }

        }

        if(j==str.length){return true;}
        else{return false;}
    }
}

那上边的后两个if也可以用递归,并且也可以合并,用递归之后最外面的for循环也没用了,改着改着就越来越像牛客网@披萨大叔的解答如下。

public class Solution {
  public boolean match(char[] str, char[] pattern) {
    if (str == null || pattern == null) {
        return false;
    }
    int strIndex = 0;
    int patternIndex = 0;
    return matchCore(str, strIndex, pattern, patternIndex);
  }

public boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
    //有效性检验:str到尾,pattern到尾,匹配成功
    if (strIndex == str.length && patternIndex == pattern.length) {
        return true;
    }
    //pattern先到尾,匹配失败
    if (strIndex != str.length && patternIndex == pattern.length) {
        return false;
    }
    //模式第2个是*,且字符串第1个跟模式第1个匹配,分3种匹配模式;如不匹配,模式后移2位
    if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
        if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) {
            return matchCore(str, strIndex, pattern, patternIndex + 2)//模式后移2,视为x*匹配0个字符
                    || matchCore(str, strIndex + 1, pattern, patternIndex);//*匹配1个,再匹配str中的下一个
        } else {
            return matchCore(str, strIndex, pattern, patternIndex + 2);
        }
    }
    //模式第2个不是*,且字符串第1个跟模式第1个匹配,则都后移1位,否则直接返回false
    if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) {
        return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
    }
    //要么str越界,要么a与b不匹配
    return false;
    }
}

相对比就发现,自己构造一个函数(形参包含索引)来递归,比用题目原给的match函数(形参就是两个数组)来递归要好的多,不然你递归这个match函数还得构造新的str和pattern函数,麻烦。
另一位同学牛客网@跪求offer养家糊口用C++,同样的思路,不得不说很简洁,解释得也很好,点赞!

/*
    解这题需要把题意仔细研究清楚,反正我试了好多次才明白的。
    首先,考虑特殊情况:
         1>两个字符串都为空,返回true
         2>当第一个字符串不空,而第二个字符串空了,返回false(因为这样,就无法
            匹配成功了,而如果第一个字符串空了,第二个字符串非空,还是可能匹配成
            功的,比如第二个字符串是“a*a*a*a*”,由于‘*’之前的元素可以出现0次,
            所以有可能匹配成功)
    之后就开始匹配第一个字符,这里有两种可能:匹配成功或匹配失败。但考虑到pattern
    下一个字符可能是‘*’, 这里我们分两种情况讨论:pattern下一个字符为‘*’或
    不为‘*’:
          1>pattern下一个字符不为‘*’:这种情况比较简单,直接匹配当前字符。如果
            匹配成功,继续匹配下一个;如果匹配失败,直接返回false。注意这里的
            “匹配成功”,除了两个字符相同的情况外,还有一种情况,就是pattern的
            当前字符为‘.’,同时str的当前字符不为‘\0’。
          2>pattern下一个字符为‘*’时,稍微复杂一些,因为‘*’可以代表0个或多个。
            这里把这些情况都考虑到:
               a>当‘*’匹配0个字符时,str当前字符不变,pattern当前字符后移两位,
                跳过这个‘*’符号;
               b>当‘*’匹配1个或多个时,str当前字符移向下一个,pattern当前字符
                不变。(这里匹配1个或多个可以看成一种情况,因为:当匹配一个时,
                由于str移到了下一个字符,而pattern字符不变,就回到了上边的情况a;
                当匹配多于一个字符时,相当于从str的下一个字符继续开始匹配)
    之后再写代码就很简单了。
*/
class Solution {
public:
    bool match(char* str, char* pattern)
    {
        if (*str == '\0' && *pattern == '\0')
            return true;
        if (*str != '\0' && *pattern == '\0')
            return false;
        //if the next character in pattern is not '*'
        if (*(pattern+1) != '*')
        {
            if (*str == *pattern || (*str != '\0' && *pattern == '.'))
                return match(str+1, pattern+1);
            else
                return false;
        }
        //if the next character is '*'
        else
        {
            if (*str == *pattern || (*str != '\0' && *pattern == '.'))
                return match(str, pattern+2) || match(str+1, pattern);
            else
                return match(str, pattern+2);
        }
    }
};

7、字符串的字典序

输入一个字符串,按字典序打印出该字符串中字符的所有排列,字符串中可能有重复字母,字母区分大小写。
思路一:按字典序排,可以理解为第一个按从小到大的顺序,每一个确定的第一位,后面接上按字典序排的所有可能,这就可以递归了。

import java.util.ArrayList;
public class Solution {
    public ArrayList<String> Permutation(String str) {
           char[] a=new char[str.length()];
           for(int j=0;j<str.length();j++) {
               char temp='z';
               for (int i = j; i < str.length(); i++) {
                   if (str.charAt(i) <temp){temp=str.charAt(i);}
               }
               a[j]=temp;
           }
           String A=String.valueOf(a);//字符数组变字符串,还可以String A=new String(a);
           //先排一遍序,以后就不需要再排了
       return f(A);

    }

    ArrayList<String> f(String s){
        ArrayList<String> AL=new ArrayList();

        if(s.length()==1){AL.add(s);return AL;}//递归的终点

        for(int i=0;i<s.length();i++){//第一位按升序来
            StringBuffer sb = new StringBuffer (s);
            StringBuffer sb1=sb.replace(i,i+1,"");//StringBuffer的replace有替换制定位置的字符段的功能,String的replace只能替换某种字符串(可能很多重复的都换了)
            String s1=sb1.toString();//s1是除掉第一个位置上的字母后剩下的字母的顺序字符串

            ArrayList<String> AL1=f(s1);//得到第一位后面的字典序
            for(int m=0;m<AL1.size();m++){
                String st=s.substring(i,i+1)+AL1.get(m);//对确定的每个第一位,将其与后面所有的字典序拼接起来
                AL.add(st);
            }
            while(i+1<s.length()&&s.charAt(i+1)==flag){i++;}//第一位的字母不能重复,重复就跳过,这是在去重
        }
        return AL;
    }
}

牛客网@牛客000001号的解答中,有一种经典解法,与我的思想一致,但实现形式不同。

//解答1
vector<string> Permutation(string str) {
        sort(str.begin(), str.end());//也是首先排个序
        vector<string> ans;
        PermutationHelp(ans, 0, str);
        return ans;
    }

void PermutationHelp(vector<string> &ans, int k, string str){
        if(k == str.size() - 1)
            ans.push_back(str);//到最后一位了递归结束
        for(int i = k; i < str.size(); i++){//将自己与从自己开始(包括自己)后面的每个字符交换
            if(i != k && str[k] == str[i])//如果遇到其他的(即不是自己对自己)与自己相同的字符,不用交换,这就是在去重
                continue;
            swap(str[i], str[k]);//如果最开始就是有序的,交换之后得到一种效果,第一位是在递增的,而且后面的全部字符都是有序的
            PermutationHelp(ans, k + 1, str);//这里是按值传递str传进去了,递归里面的操作是操作的str副本,不影响现在这个层次的str,也就是说,在for循环的下一次运行,下一次swap时,所操作的str和上次swap后的没有变化
        }
    }

由此可见,这swap与上面我 取出一个字母放第一位,把剩下的放后面,是一样的效果,目的都是得到一组递增的字符串,每个字符串都是 其首字母相同的这个等级 里面最小的(如12345,21345,31245,41235,51234,它们都是在一、二、三、四、五万档里面最小的),像有序的树一样。

我发现好多人对 (swap、递归、swap) 这个方法有疑问,我的理解如下:
以下图片代码来自牛客网@HAHA7877
图七.1

//解答2
import java.util.List;
import java.util.Collections;
import java.util.ArrayList;

public class Solution {
    public static void main(String[] args) {
        Solution p = new Solution();
        System.out.println(p.Permutation("abc").toString());
    }

    public ArrayList<String> Permutation(String str) {
        List<String> res = new ArrayList<>();
        if (str != null && str.length() > 0) {
            PermutationHelper(str.toCharArray(), 0, res);
            Collections.sort(res);//(关键点1)最后再排序
        }
        return (ArrayList)res;
    }

    public void PermutationHelper(char[] cs, int i, List<String> list) {
        if (i == cs.length - 1) {
            String val = String.valueOf(cs);
            if (!list.contains(val))//(关键点2)最后在加入容器之前才判断有没有跟之前的重了,重了就不加入容器。而不是从根本上解决去重问题
                list.add(val);
        } else {
            for (int j = i; j < cs.length; j++) {
                swap(cs, i, j);
                PermutationHelper(cs, i+1, list);//这里传进去的是cs和list的引用副本,里面和外面的引用指向同一个对象,更改什么是会影响到外面的
                swap(cs, i, j);
            }
        }
    }

    public void swap(char[] cs, int i, int j) {
        char temp = cs[i];
        cs[i] = cs[j];
        cs[j] = temp;
    }
}

解答2中有两个值得关注的点:
关键点1是最后调用sort方法来排字典序,连排序算法都省了,真的好吗,而且这样就表示 前面我得到的所有加入容器的字符串,管他有没有字典序,一顿暴力排就是了,有时字符串多了,性能肯定不好的,而且在sort之前只要得到全排列就行了,全排列下面还会说一下其他的实现方式。解答2需要最后用sort是没办法的,因为它得到的字符串不是字典序的,为什么呢。看图,它的cs字符数组是沿着树的线条在变化(就是在不同的递归调用里外变化,因为操作的对象始终是同一个字符数组),两个swap保证了ACB先变成ABC,再变成ABC,再变成BAC,就是从一个兄弟节点到下一个兄弟节点时,是先变成他们父节点的样子,再变成下一个兄弟节点。这样能得到全排列的结果(不会漏掉可能的字符串),如果要去重,既可以用解答2中的关键点2,还可以

//解答2的新去重版本,这是从源头解决重复问题
public void PermutationHelper(char[] cs, int i, List<String> list) {
        if (i == cs.length - 1) {
            String val = String.valueOf(cs);
                list.add(val);
        }
        else{
            Set<Character> charSet = new HashSet<Character>();//Set不包含重复项
            for (int j = i; j < cs.length; j++) {
              if(j==i || !charSet.contains(chars[j])){//与i交换的对象其值不能重复
                charSet.add(chars[j]);//被交换过了就进去备案
                swap(cs, i, j);
                PermutationHelper(cs, i+1, list);
                swap(cs, i, j);
              }
            }
        }
    }

可以发现,这种预防重复的方法与解答1中的还不太一样,解答1中是相互交换的两个值不能一样(自己可以和自己),这里是与某k位交换的所有目标里不能有相同重复的。因为解答1的递归是值传递,用图来说就是,每一层的下一个点都是由该层的上一个点生成的,每层之间的值互不影响,这样可以使得每层的所有字符串是一组递增的字符串,且每个字符串都是 其首字母相同的这个等级 里面最小的,每两个字符串之间的区间在下一层做进一步的细分。这样的话只要相互交换的两个值不一样(一样了则交换后和交换前一模一样),就不会有重复的结果(因为结果都是按升序排的,发生重复一定是一层的前一个节点生成下一个节点时发生重复,相当于交换前后一模一样)。解答2中的两个swap保证所得的结果不会漏掉某些合法的字符串(相当于全排列,把每种可能性都搞出来了,每一层确认一位数),但是它的每层是无序的,因此就不能用解答1中的那种去重方法(去不干净,还是会有重复,可用ABB验证),而且解答2中的第二个swap是必要的,如果没有它,上图的第二层的第二个点就是由第三层的第二个点生成的,不是沿着线条动,这样是会漏掉部分合法的字符串的(可用ABC来验证)。因此,引用传递的,一定要有两个swap,这样可以得到全排列,不会漏,如果说它和别的全排列有什么区别,就是它可以加上条件以实现从源头去重,但没法保证有序。有的解答用TreeSet来达到去重与排序的目的,那就太没节操了吧。

那如果强行给解答1加了第二个swap会怎样,就是每个兄弟节点变为下一个兄弟节点前,它先自旋一下,把自己变成和它父节点一样,和解答2很相似是不是,区别是解答2是一个对象在沿着线条变,这个是把自己变成和它父节点一样,但最终的结果是一样的,值传递+两个swap=引用传递+两个swap

还有一种解答是来自牛客网@牛客000001号

//解答3
void PermutationHelp(vector<string> &ans, int k, string str){
    if(k == str.size() - 1)
        ans.push_back(str);
    unordered_set<char> us;  //记录出现过的字符
    sort(str.begin() + k, str.end());  //每递归一次就排一次序,保证k后面的是升序
    for(int i = k; i < str.size(); i++){
        if(us.find(str[i]) == us.end()) //只和没交换过的换
        {  
            us.insert(str[i]);
            swap(str[i], str[k]);
            PermutationHelp(ans, k + 1, str);
            swap(str[i], str[k]);  //复位
        }
    }
}

vector<string> Permutation(string str) {
    vector<string> ans;
    PermutationHelp(ans, 0, str);
    return ans;
}

解答3先搞第二个swap复位导致在下一轮swap时k后面不是升序(不要第二个swap反而k后面的是升序,噢,这个的前提是 最开始得排一次序+值传递),再在递归里面加一个sort来使k后面的是升序,属于先制造一个麻烦再解决这个麻烦。

思路二:全排+去重+排序,虽然我不喜欢这种思想,但是还是有很多有意思的全排方法。上面的引用传递+两个swap就是全排列,它的优点是加上判断条件能从源头去重,下面的两个全排列本身不能去重。
来自牛客网@Jamesli

import java.util.*;
public class Solution {
    private char [] seqs;
    private Integer [] book;//标记还剩下的可以填位的数

    private HashSet<String> result = new HashSet<String>();//用Set来去重

    public ArrayList<String> Permutation(String str) {
        ArrayList<String> arrange = new ArrayList<String>();
        if(str == null || str.isEmpty()) return arrange;
        char[] strs = str.toCharArray();
        seqs = new char[strs.length];
        book = new Integer[strs.length];
        for (int i = 0; i < book.length; i++) {
            book[i] = 0;//为0就是可被选择
        }
        dfs(strs, 0);
        arrange.addAll(result);
        Collections.sort(arrange);//用sort来排序
        return arrange;
    }

    private void dfs(char[] arrs, int step){
        //已走完所有可能
        if(arrs.length == step){
            String str = "";
            for (int i = 0; i < seqs.length; i++) {
                str += seqs[i];
            }
            result.add(str);
            return; //返回上一步
        }
        //对某一个坑位,遍历可以放进去的数
        for (int i = 0; i < arrs.length; i++) {
            //还没被放进去才能备选
            if(book[i] == 0){
                seqs[step] = arrs[i];//对于这一种可能
                book[i] = 1;//这个数已经用了,后面的坑位不能再用这个数
                dfs(arrs, step + 1);//递归来确定后面的坑位怎么填
                book[i] = 0;//把这个数放出来备用,因为准备尝试换下一个数填进这个坑位
            }
        }
    }
}

这个方法遍历了每个位置的每种可能,它需要标记,因为需要保证不会将同一个东西放在多个坑位上(又不能分身),之前的交换那种,因为不同的数总是放在不同坑位,只是换来换去,就不会担心这种问题。

来自牛客网@亲切年

public class Solution {
    public ArrayList<String> Permutation(String str) {
        TreeSet<String> tree = new TreeSet<>();//用TreeSet去重+排序
        Stack<String[]> stack = new Stack<>();
        ArrayList<String> results = new ArrayList<>();
        stack.push(new String[]{str,""});
            do{
                String[] popStrs = stack.pop();
                String oldStr = popStrs[1];
                String statckStr = popStrs[0];
                for(int i =statckStr.length()-1;i>=0;i--){
                    String[] strs = new String[]{statckStr.substring(0,i)+statckStr.substring(i+1),oldStr+statckStr.substring(i,i+1)};
                    if(strs[0].length()==0){
                        tree.add(strs[1]);
                    }else{
                        stack.push(strs);
                    }
                }
            }while(!stack.isEmpty());
        for(String s : tree)
            results.add(s);
        return results;
    }
}

上面这种方法的思想是,对于第一位,从字符串中分一个出来,将每种可能压入栈中,然后弹一个栈顶出来,对弹出来的这个第一位已确定的情况,再将字符串后面的部分分一个出来给第二位,这样就确定了前两位,把这一轮(第一位确定,第二位所有可能)的所有可能压入栈中,接着还是这样干,直到每个字符串后面的数都被分完了,也就是字符串的每一位都确定了,就加入容器,不入栈了,如此重复下去直至栈空。

这两种全排列分别是用递归和栈实现的。

思想三:字典序排列算法
来自牛客网@天天502

public ArrayList<String> Permutation2(String str){
        ArrayList<String> list = new ArrayList<String>();
        if(str==null || str.length()==0){
            return list;
        }
        char[] chars = str.toCharArray();
        Arrays.sort(chars);//最开始要排序
        list.add(String.valueOf(chars));//最小的
        int len = chars.length;
        while(true){
            int lIndex = len-1;
            int rIndex;
            while(lIndex>=1 && chars[lIndex-1]>=chars[lIndex]){//找到lIndex,是从右到左一直递增的最高峰
                lIndex--;
            }
            if(lIndex == 0)//从右到左一直递增,那这就是最大值了,不用再找也不用再交换什么,这个最大值已经在上个循环被加入list了
                break;
            rIndex = lIndex;//用rIndex来找从lIndex开始右边中比lIndex-1要大的最小值,那自然是从lIndex开始从大到小找
            while(rIndex<len && chars[rIndex]>chars[lIndex-1]){
                rIndex++;
            }
            swap(chars,lIndex-1,rIndex-1);//交换后从lIndex开始的右边仍然是递减的
            reverse(chars,lIndex);

            list.add(String.valueOf(chars));
        }

        return list;
    }

    private void reverse(char[] chars,int k){
        if(chars==null || chars.length<=k)
            return;
        int len = chars.length;
        for(int i=0;i<(len-k)/2;i++){
            int m = k+i;
            int n = len-1-i;
            if(m<=n){
                swap(chars,m,n);
            }
        }

    }

从一个数到下一个比它大的数,从后面往前找,分为AB两段,B段递减说明这个AB肯定是A档里面最大的,下一个数肯定是下一档里面最小的,下一档怎么确定,肯定是把A的末尾数a变大,所以就在B段里面找备选的数值,怎样符合要求呢,应该是比a大的所有数中最小的那个数b,把b和a交换,A也提升了一个档次,B仍然是递减的,将B翻转变成递增的,就是新A档里面最小的数了。

8、链表中倒数第k个结点

输入一个链表,输出该链表中倒数第k个结点。
思路一:快慢指针均指向head,先让快指针走到一个地方,从那个地方的倒数第k个节点正是head,然后快慢指针一起走,当快指针是最后一个时,慢指针就是整个链表的倒数第k个节点。

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/

public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        if(head==null||k==0) return null;

        ListNode a=head;
        for(int i=0;i<k-1;i++){//走k-1步
             a=a.next;
            if(a==null) return null;//还没走完就没得走?k太大超了呗
        }

        ListNode c=head;
        while(a.next!=null){//一起走
            a=a.next;
            c=c.next;
        }
        return c;
    }
}

下面来自牛客网@Aurora1,同样的思想,更加简洁精炼

public ListNode FindKthToTail(ListNode head,int k) {
        ListNode p, q;
        p = q = head;
        int i = 0;
        for (; p != null; i++) {
            if (i >= k)//k=0的话,q最后会变成尾节点后的null,非常巧妙,但最开始判断下k就不用遍历了,所以代码不是越少越好
                q = q.next;
            p = p.next;
        }
        return i < k ? null : q;
    }

思路二:我最开始想过,翻转链表再遍历,又想了一下如果链表很长就太费时;或者先遍历一遍找到链表长度,再计算倒数第几位是正数第几位,这相当于遍历两次;又或者用栈,按链表顺序压进栈,再弹出几个,但链表很长的话,搞不好栈空间要溢出,而且这也相当于遍历两次。这几个方法全都不太好。
思路三:来自牛客网@黎离,运用递归,非常的巧妙!!!

unsigned int cnt=0;//在函数外面
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
    if(pListHead==NULL)//递归结束的条件
        return NULL;
    //该函数传入的节点,从尾节点后面的null开始,返回的是null,再往前还是空,当传入的是倒数第k-1个节点,函数返回其前驱,即倒数第k个节点,再往前一直都返回同一个节点,即倒数第k个节点
    ListNode* node=FindKthToTail(pListHead->next,k);
    if(node!=NULL)
        return node;
    cnt++;
    if(cnt==k)
        return pListHead;
    else
        return NULL;
}

9、丑数

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
思想一:联想到自己关于素数做的总结(传送门),想用筛的方法,但其实这样并不好,既有很严重的重复标记问题,还花费很多的空间(235相乘,后期后两个数之间的跨度会很大,这可不像素数),运行时显示 堆溢出Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

public class Solution {
    public int GetUglyNumber_Solution(int index) {

        if(index==0){return 0;}
        else if(index==1){return 1;}
        else if(index==2){return 2;}
        else if(index==3){return 3;}
        else if(index==4){return 4;}
        else if(index==5){return 5;}//不得不说这一段写的是真的烂
        else if(index>=6){

            int size;
            if(index>=1500){size=400*index*index;}//数组长度太大了
            else{size=index*index;}
            boolean[] flag=new boolean[size+1];//大数组对虚拟机是很炒蛋的事
            int[] num=new int[index];
            num[0]=1;num[1]=2;num[2]=3;num[3]=4;num[4]=5;
            flag[6]=true;flag[9]=true;flag[8]=true;flag[12]=true;flag[16]=true;
            flag[10]=true;flag[15]=true;flag[20]=true;flag[25]=true;
            int k=5;

            for(int i=6;i<=flag.length;i++){
                if(flag[i]){//标记了就是丑数
                    num[k]=i;
                    for(int j=1;j<=k&&i*num[j]<flag.length;j++){//将该丑数与它前面的所有丑数相乘的乘积标记为丑数
                        flag[i*num[j]]=true;
                    }

                if(k==index-1){return num[k];}//直到找到第index个丑数
                else{k++;}
                }
            }   
        }
        return 0;
 }

思想二:丑数肯定都是在丑数的基础上乘以235得到的,容易想到
来自牛客网@scut_huajian

 int GetUglyNumber_Solution(int index) {
        int* array= new int[index+1];
        switch(index){
            case 1:
            return 1;
            case 2:
            return 2;
            case 3:
            return 3;
            case 4:
            return 4;
            case 5:
            return 5;//同样很烂
        }
        array[0]=1;
        array[1]=2;
        array[2]=3;
        array[3]=4;
        array[4]=5;

        for(int i=5;i<index;i++)
        {
            int min=0;
            int min2=0,min3=0,min5=0;
            int temp=0;
            for(int j=0;j<i;j++)//每次总是从头开始找大于 最后一个丑数 的最小的丑数
            {
                temp=array[j]*2;
                if(temp>array[i-1])
                {min2=temp;break;}
            }
           for(int j=0;j<i;j++)
            {
                temp=array[j]*3;
                if(temp>array[i-1])
                {min3=temp;break;}
            }
            for(int j=0;j<i;j++)
            {
                temp=array[j]*5;
                if(temp>array[i-1])
                { min5=temp;break;}
            }
           min=min2>min3?(min3>min5?min5:min3):(min2>min5?min5:min2);
            array[i]=min;
        }
        return array[index-1];
    }

很明显,每次都要从丑数数组的起点开始,做了很多无效的操作啊,比如说,前面找丑数时,2乘以丑数数组的前十个都比最后一个丑数小,那当找出下一个丑数时,丑数肯定是越来越大的,这时还从丑数数组的起点开始?拜托前十个乘以2绝对比新丑数更小啦。这样想的话,其实我们可以维护三个数组下标,让下标在合适的时候往后移。

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index>=0&&index<=6){return index;} //一条句子能做的事为什么要写七条
        else if(index>=7){
            int count2=0,count3=0,count5=0; //数组下标
            int[] num=new int[index];
            num[0]=1;
            for(int k=1;k<index;k++){
            int min=2*num[count2]<3*num[count3]?2*num[count2]:3*num[count3];
            num[k]=min<5*num[count5]?min:5*num[count5];
            if(num[k]==2*num[count2]){count2++;}//当某个数选为新的丑数,下标就要后移了
            if(num[k]==3*num[count3]){count3++;}//可以去重的
            if(num[k]==5*num[count5]){count5++;}
            }
            return num[index-1];
        }     
        return 0;  
    }
}

思想三:维护三个队列
1)初始化array和队列:Q2 Q3 Q5
2)将1插入array
3)分别将1*2、1*3 、1*5插入Q2 Q3 Q5
4) 令x为Q2 Q3 Q5中的最小值,将x添加至array尾部
5)若x存在于:
Q2:将 x * 2、x * 3、x*5 分别放入Q2 Q3 Q5,从Q2中移除x
Q3:将 x * 3、x*5 分别放入Q3 Q5,从Q3中移除x
Q5:将 x * 5放入Q5,从Q5中移除x
6)重复步骤4~6,知道找到第k个元素
来自牛客网@rollsyang

int GetUglyNumber_Solution(int index) {
    if (index < 1)
        return NULL;

    int minVal = 0;
    queue<int> q2, q3, q5;
    q2.push(1);

    for (int i = 0; i < index; i++)
    {
        int val2 = q2.empty() ? INT_MAX : q2.front();
        int val3 = q3.empty() ? INT_MAX : q3.front();
        int val5 = q5.empty() ? INT_MAX : q5.front();

        minVal = min(val2, min(val3, val5));

        if (minVal == val2)
        {
            q2.pop();
            q2.push(2 * minVal);
            q3.push(3 * minVal);
        }
        else if (minVal == val3)
        {
            q3.pop();
            q3.push(3 * minVal);
        }
        else
        {
            q5.pop();
        }

        q5.push(5 * minVal);
    }

    return minVal;
}

丑数分解为235的乘积后,不同的丑数235的个数肯定是不同的,Q2全是若干个2的乘积,Q3是若干个3的、若干个2和3的乘积,Q5是若干个5、若干个2和5、若干个3和5、若干个235的乘积。可见,三个队列各自是递增的,且不会有重复的丑数出现。

10、最小的k个数

输入n个整数,找出其中最小的K个数。
思想一:用堆来做,维护一个最大堆,其容量为k,让每个整数都与堆顶数字比较,比堆顶小就让这个数字成为堆顶,然后调整堆。重复下去。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> A=new ArrayList<>();
        if(k>input.length||k<=0)return A;//特殊情况

         int[] heap=new int[k+1];//用数组来当最大堆
         for(int i=1;i<k+1;i++){heap[i]=Integer.MAX_VALUE;}//最开始堆里面都是最大值,也可以直接把input里面的前k个放进来当堆的初始值

         for(int j=0;j<input.length;j++){
             if(input[j]<heap[1]){
                 heap[1]=input[j];
                 tune(heap);//调整堆
             }
         }
        find(A,heap,1);//最后堆里面存的是k个最小值,这k个数是按堆的数据结构存的,得按一定得顺序取出来才能得到升序。或者再把它们排一次序
        return A;

    }

    //这个最大堆是完全二叉树,父节点比子节点大,若有两个子节点则左边的比右边小
    void find(ArrayList<Integer> a,int[] h,int c){
        if(2*c<h.length){//c这个节点还有左子节点
            find(a,h,2*c);//递归
            if(2*c+1<h.length){find(a,h,2*c+1);}//在c节点还有左子节点的前提下,它还有右子节点,递归
        }
        a.add(h[c]);//子节点处理完后,才是父节点
        return;
    }

    //调整最大堆
    void tune(int[] h){
        for(int m=1;m<=(h.length-1)/2;m++){  //(h.length-1)/2是最后一个非叶子节点,所以m肯定是有子节点的
            if(2*m+1>=h.length){   //若m这个点没有右子节点
                if(h[m]<h[2*m]) {
                    swap(h, m, 2 * m);
                }
            }
            else{                  //m这个点有两个子节点
                if(h[2*m]>h[2*m+1]){    //左子节点比较大
                    if(h[2*m]>h[m]){swap(h,m,2*m);}   //子节点中大的比父节点大,就互换
                }
                else{                   //右子节点比较大
                    if(h[2*m+1]>h[m]){swap(h,m,2*m+1);}
                }
                if(h[2*m]>h[2*m+1]){swap(h,2*m,2*m+1);}  //保持左子节点比右子节点小
            }
        }
    }

    void swap(int[] h,int m,int n){
        int temp=h[m];
        h[m]=h[n];
        h[n]=temp;
    }
}

我写的这个答案比较古朴,Java中PriorityQueue就是用堆实现的,因此还可用优先队列来做
来自牛客网@披萨大叔

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
   public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
       ArrayList<Integer> result = new ArrayList<Integer>();
       int length = input.length;
       if(k > length || k == 0){
           return result;
       }

        PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {//重写比较器,默认是最小堆,改为最大堆顶
                return o2.compareTo(o1);
            }

        for (int i = 0; i < length; i++) {
            if (maxHeap.size() != k) {
                maxHeap.offer(input[i]);
            } else if (maxHeap.peek() > input[i]) {
                Integer temp = maxHeap.poll();
                temp = null;   //方便GC
                maxHeap.offer(input[i]);
            }
        }
        //注意下面两者有何不同
        for(int i=0;i<k;i++){
            res[k-i-1] = priorityQueue.poll();
        }
         /*  
        for (Integer integer : maxHeap) {
            result.add(integer);
        }*/
        return result;
    }
}

需要注意的是PriorityQueue对元素采用的是堆排序,头是按指定排序方式的最大或小元素。堆排序只能保证根是最大(最小),整个堆并不是有序的。也就是说,从优先队列中取出或添加元素的时间复杂度为O(log(n)) ,因为插入或删除后都要调整堆。方法iterator()中提供的迭代器可能只是对整个数组的依次遍历,也就只能保证数组的第一个元素是最小的。

思想二:这个问题跟TOP-K问题一样,可以冒泡,可以选择排序,可以插入排序,时间复杂度为O(n*k);可以维护一个堆,时间复杂度为O(n*logk);可以用改进后的快排,处理TOP-K的BFPRT算法,可以用O(n)的时间复杂度找到第k个大的数,及比它小的前k个数(不是有序的),所以如果还需要前k个数有序,还得自己再排一下。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值