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

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

目录

1、找出二叉搜索树中的第k小的结点

思路一:中序遍历,栈

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

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

    }

}
*/

import java.util.Stack;
public class Solution {
    TreeNode KthNode(TreeNode pRoot, int k){ 
        if(pRoot==null)return null;

        int count=0;
        Stack<TreeNode> s=new Stack();
        TreeNode t=pRoot;
        s.add(t);
        //先找到最左的
        while(t.left!=null){
            t=t.left;
            s.add(t);
        }
        while(!s.isEmpty()){
            t=s.pop();
            if(++count==k){return t;}//计数,如果它是第k个,返回
            if(t.right!=null){//右子节点不为空
                t=t.right;
                s.add(t);
                while(t.left!=null){//那么从这个右子节点开始,还是要先一路左到底
                    t=t.left;
                    s.add(t);
                }       
            }     
        }
        return null;        
    }
}

来自牛客网@老石基

TreeNode KthNode(TreeNode root, int k){
        if(root==null||k==0)
            return null;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        int count = 0;
        TreeNode node = root;
        do{
            if(node!=null){
                stack.push(node);
                node = node.left;
            }else{
                node = stack.pop();
                count++;
                if(count==k)
                    return node;
                node = node.right;
            }
        }while(node!=null||!stack.isEmpty());
        return null;
    }

思路二:中序遍历,递归

public class Solution {
    TreeNode KthNode(TreeNode pRoot, int k){ 
       if(pRoot==null){return null;}

        return midFind(pRoot,k);
    }

   int count=0;//全局变量
   TreeNode midFind(TreeNode t,int k){
        if(t==null)return null;

        TreeNode t1=midFind(t.left,k);//没找到第k个就返回空
        if(t1!=null)return t1;//前面找到了第k个就返回它

        if(++count==k)return t;//自己是第k个,返回自己

        TreeNode t2=midFind(t.right,k);
        if(t2!=null)return t2;

        return null;//都没找到就返回空
    }
}

2、第一个只出现一次的字符

在一个字符串中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。
思路一:我先想到的是筛减法,对每个字符都筛掉它后面的与它相同的字符,但是这种对每个字符串都需要比较的方法,这种类型的方法在极端情况下时间复杂度是 O(n2) O ( n 2 ) ,它不像素数肯定是有东西可以筛的,如果整个字符串全不一样,没得筛,那就和暴力遍历没区别。

public class Solution {
    public int FirstNotRepeatingChar(String str) {
        boolean[] flag=new boolean[str.length()];
        boolean mark=false;
        for(int i=0;i<str.length();i++){
            if(!flag[i]){
                for(int j=i+1;j<str.length();j++){
                        if(!flag[j]&&str.charAt(i)==str.charAt(j)){
                            flag[j]=true;//需要筛掉的标记为true
                            mark=true;
                        }
                }
                if(mark){flag[i]=true;mark=false;}//有重复的把本身也筛掉
            } 
        }

        for(int m=0;m<str.length();m++){
            if(!flag[m]){return m;}
        }
        return -1;
    }
}

思路二:由于要把整个字符串都遍历完了才能得到字符串的重复情况,所以遍历一遍,边遍历边记录,给每个不同的字符计数,对于遍历到的一个字符,如何改变它所属字符的计数,如何找到归属?一是用hashmap;二是利用字符可以变为ASCII码的特性,这是线性时间复杂度方法

来自牛客网@LoveNX

import java.util.HashMap;
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        HashMap <Character, Integer> map = new HashMap<Character, Integer>();

        for(int i=0;i<str.length();i++){
            if(map.containsKey(str.charAt(i))){
                int time = map.get(str.charAt(i));
                map.put(str.charAt(i), ++time);
            }
            else {
                map.put(str.charAt(i), 1);
            }
        }  

        for(int i=0;i<str.length();i++){
            char c = str.charAt(i);
            if (map.get(c) == 1) {
                return i;
            }
        }
        return -1;
    }
}

来自牛客网@victorian33

public class Solution {
    public int FirstNotRepeatingChar(String str) {
        char[] chars = str.toCharArray();
        int[] map = new int[256];//ASCII表种各种不同的字符一共有256种
        for (int i = 0; i < chars.length; i++) {
            map[chars[i]]++;
        }
        for (int i = 0; i < chars.length; i++) {
            if (map[chars[i]] == 1) return i;
        }
        return -1;
    }
}

3、奇数在前偶数在后相对位置不变

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
思路一:空间换时间,空间复杂度 O(n) O ( n ) ,时间复杂度 O(n) O ( n ) ,感觉有点low

import java.util.ArrayList;
public class Solution {
    public void reOrderArray(int [] array) {
        ArrayList<Integer> even=new ArrayList();
        int j=0;
        for(int i=0;i<array.length;i++){
            if(array[i]%2==0){even.add(array[i]);}
            else{array[j++]=array[i];}
        }
        for(int m=0;m<even.size();m++){
            array[j++]=even.get(m);
        }
    }
}

思路二:空间复杂度 O(1) O ( 1 ) ,时间复杂度 O(n2) O ( n 2 ) ,采用类似插入排序、冒泡排序的方法。

import java.util.ArrayList;
public class Solution {
    public void reOrderArray(int [] array) {
        for(int i=0;i<array.length;i++){
            //注意数组越界问题
            if((array[i]&1)==1&&i-1>=0&&(array[i-1]&1)==0){//有一个奇数在偶数后面
                int temp=array[i];//取出这个奇数
                int j=i-1;
                while(j>=0&&(array[j]&1)==0){//把这个奇数前面的偶数全部后移一位
                    array[j+1]=array[j];
                    j--; 
                }
                array[j+1]=temp;//将这个奇数放到前面
            } 
        }     
    }
}

4、扑克牌顺子

一副牌四张癞子,癞子为0,外加1到13,一次抽五张牌,能不能成顺子。
思路一:要想成顺子,有两个条件,抽到的牌里面,除0外没有重复的数,除0外的max – min < 5。我考虑的是先排序,利用排好序后的序列来判断有没有对子,排好序后最大值最小值也出来了。

public class Solution {
    public boolean isContinuous(int [] numbers) {
        int l=numbers.length;
        if(l==0)return false;

        int count=0;
        for(int i=0;i<l-1;i++){//冒泡排序
            for(int j=0;j<l-i-1;j++){
                if(numbers[j+1]<numbers[j]){
                    int temp=numbers[j+1];
                    numbers[j+1]=numbers[j];
                    numbers[j]=temp;
                }
            }
        }

        for(int i=0;i<l;i++) {//只是为了找到最小值的下标
            if (numbers[i] == 0) {
                count++;
            }
        }
        //如果 存在对子 或 最大值太大,则凑不出顺子
        if((count+1!=l&&numbers[l-1]-numbers[count]<l-count-1)||numbers[count]+l-1<numbers[l-1]){
        return false;
        }
        else{return true;}
    }
}

思路二:不用排序,判断对子用统计的方法,最大最小值在遍历过程中确定。
来自牛客网@Xy。

public class Solution {
    public boolean isContinuous(int [] numbers) {
        if(numbers.length != 5) return false;
        int min = 14;
        int max = -1;
        int flag = 0;
        for(int i = 0; i < numbers.length; i++) {
            int number = numbers[i];
            if(number < 0 || number > 13) return false;
            if(number == 0) continue;
            //Bit-map,看看这个位子有没有标记为1,有就说明重复了,有对子
            if(((flag >> number) & 1) == 1) return false;
            flag |= (1 << number);//没有就标记为1
            if(number > max) max = number;//顺便找下最大最小值
            if(number < min) min = number;
            if(max - min >= 5) return false;
        }
        return true;
    }
}

用set来判断是否重复,感觉有点过于依赖容器了。
来自牛客网@lalala、

import java.util.HashSet;
import java.util.Set;
public class Solution {
    public boolean isContinuous(int [] numbers) {
        if(numbers.length == 0) return false;
        Set<Integer> set = new HashSet<>();
        int maxn = -1, minn = 14;
        for(int a: numbers){
            if(!set.add(a) && a!=0) return false;
            if(a!=0) {
                maxn = Math.max(maxn, a);
                minn = Math.min(minn, a);
            }
        }
        if(maxn - minn <=4) return true;
        return false;        
    }
}

思路三:用统计去重,找最小值,看癞子够不够凑到最大值。
来自牛客网@Nobody_Zheng

public boolean isContinuous(int [] numbers) {
        int[] counts = new int[18];
        for (int num:numbers)   counts[num]++;
        int left,i;
        for (left = 1; left <14; left++) {
            if (counts[left] != 0)  break;
        }
        //用left记下最小的一个非0数字
        for (i = left + 1; i <= left+4; i++) {
            if (counts[i] == 0){
                if (counts[0] == 0) return false;
                else                counts[0]--;
            }
        }
        return true;
    }

5、字符串转换成整数

输入一个字符串,包括数字字母符号,可以为空,如果是合法的数值表达则返回该数字,否则返回0。第一位可以是正负号。
思路一

public class Solution1 {
    public int StrToInt(String str) {
        if(str==null||str.length()==0)return 0;

        int num=0;
        int e=0;
        int sign=1;
        if(str.charAt(0)=='-'){sign=-1;}

        for(int i=str.length()-1;i>=0;i--){
            if(i==0&&(str.charAt(i)=='+'||str.charAt(i)=='-')){break;}
            if(str.charAt(i)-'0'<0||str.charAt(i)-'9'>0){return 0;}
            //Math.pow(a,b)输入的是浮点数double,输出的也是浮点数double,这里要转换成int
            num+=(str.charAt(i)-'0')*(int)Math.pow(10,e++);
        }
        return num*sign;
    }
}

需要注意的是,当字符串表示的数在整型数范围内-2147483648~2147483647时,输出是与字符串表示的相符的,若超出了这个范围,本来应该显示0,但这里显示的是一个错误的数值,也就是说这个代码没有防止溢出的功能。

当输入的是-2147483648时,我们是最后才乘以符号,那在return之前,num=(int)2147483648,这里溢出了,于是强行截断再将值赋给num,此时num的二进制是(第32位为1,后面31个0),于是num=-2147483648,然后num*sign这个值仍是整型(整型*整型=整型),所以num*sign=(int)2147483648,所以返回的值是-2147483648,与字符串表示的相符。
看下面这个例子

public class Main2 {
    public static void main(String[] args){
        byte a=(byte)128;//a=-128
        byte b=-1;
        System.out.println(a);//输出-128
        System.out.println(a*b);//输出128,因为a*b是整型,两个byte相乘默认等于整型!!!!
        byte c=(byte)(a*b);//效果跟byte a=(byte)128;是一样的
        System.out.println(c);//输出-128
    }
}

那如果要加上判断溢出的功能呢,看下面

public class Solution1 {
    public int StrToInt(String str) {
        if(str==null||str.length()==0)return 0;

        long num=0;//扩大一个等级
        int e=0;
        int sign=1;
        if(str.charAt(0)=='-'){sign=-1;}

        for(int i=str.length()-1;i>=0;i--){
            if(i==0&&(str.charAt(i)=='+'||str.charAt(i)=='-')){break;}
            if(str.charAt(i)-'0'<0||str.charAt(i)-'9'>0){return 0;}
            //Math.pow(a,b)输入的是浮点数double,输出的也是浮点数double,这里要转换
            num+=(str.charAt(i)-'0')*(int)Math.pow(10,e++);
            //判断是不是超出了整型的范围
            //需要注意的是如果num还是int,那num*sign就算超过了整型范围,它是会自动截断的,所以你判断截断后的(那肯定在整型范围里面啊)有什么意义呢
            if(num*sign>Integer.MAX_VALUE||num*sign<Integer.MIN_VALUE)return 0;
        }
        //将没有超过整型范围的long转换成int,这里没有精度损失
        return new Long(num*sign).intValue();
    }
}

有个解答试图不通过“扩容”来实现溢出功能,我们来分析下对不对
来自牛客网@JacobGo!

public class Solution {
    public static int StrToInt(String str) {
        if (str == null || str.trim().equals("")) {
            return 0;
        }
        // symbol=0,说明该数为正数;symbol=1,该数为负数;start用来区分第一位是否为符号位
        int symbol = 0;
        int start = 0;
        char[] chars = str.trim().toCharArray();
        if (chars[0] == '+') {
            start = 1;
        } else if (chars[0] == '-') {
            start = 1;
            symbol = 1;
        }
        int result = 0;
        for (int i = start; i < chars.length; i++) {
            if (chars[i] > '9' || chars[i] < '0') { return 0; }

            int sum= result * 10 + (int) (chars[i] - '0');
            //这个检测方法不可靠
            if((sum-(int) (chars[i] - '0'))/10!=result){return 0;}

            result=result * 10 + (int) (chars[i] - '0');
        }
        result = (int) Math.pow(-1, symbol) * result;
        return result;
    }
}

下面说下为什么这个检测方法不行

  • 当num=(int)2147483648,num实际上为-2147483648,再减个8,则(int)-2147483656为2147483640,除以10刚好等于214748364,完美绕过检测。不过它能过检测也有一定的情有可原,因为字符串如果为-2147483648实际没溢出,你总不能用2147483648把它判定为溢出吧,但2147483648明明溢出了却检测不到(没办法,这个检测方法处理字符串-2147483648和+-2147483648是没区别的)
  • 当num=(int)2147483649,num实际上为-2147483647,再减个9,则(int)-2147483656为2147483640,除以10刚好等于214748364,完美绕过检测
  • 当num=(int)2147483650,num实际上为-2147483646,再减个0,则(int)-2147483646就是-2147483646,除以10等于-214748364.6,拦截溢出成功

还有没有其他的实际溢出但绕过检测的,我还不确定。所以判断溢出,还是老老实实用long吧。

思路二:不考虑溢出,看看另一种风格的代码
来自牛客网@马客(Mark)

class Solution {
public:
    int StrToInt(string str) {
        int n = str.size(), s = 1;
        long long res = 0;//考虑到-2147483648的情况
        if(!n) return 0;
        if(str[0] == '-') s = -1;
        //从前往后,连指数e这个变量也省了
        for(int i = (str[0] ==  '-' || str[0] == '+') ? 1 : 0; i < n; ++i){
            if(!('0' <= str[i] && str[i] <= '9')) return 0;
            //移位 2倍+8倍+ASCII码后四位=10*res+0至9的字符所代表的数值,骚操作
            res = (res << 1) + (res << 3) + (str[i] & 0xf);//res=res*10+str[i]-'0';
        } 
        return res * s;
    }
};

6、二叉树中和为某一值的所有路径

输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
思路一:这本质是带记忆的DFS搜索。可以用递归

/** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this.val = val; } } */
import java.util.ArrayList;
import java.util.LinkedList;
public class Solution {
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        ArrayList<ArrayList<Integer>> a=new ArrayList<>();
        if(root==null)return a;
        //我本意是想,这个经常会进进出出,可能ArrayList频繁增删不好?但是这个是在末尾增删,其实用ArrayList也行的
        LinkedList<Integer> L=new LinkedList<>();//动态地记录当前路径的记忆
        f(root,target,0,L,a);
        return a;
    }

    void f(TreeNode t,int tar,int c,LinkedList<Integer> L,ArrayList<ArrayList<Integer>> AA){
        L.add(t.val);
        if(t.left==null&&t.right==null){
            if(t.val+c==tar){
                ArrayList<Integer> A=new ArrayList<>();
                A.addAll(L);//合法的路径赋给一个新建的ArrayList
                AA.add(A);//存起来 
            }            
        }
        else {
            if (t.left != null) {
                f(t.left, tar, t.val + c, L, AA);
            }
            if (t.right != null) {
                f(t.right, tar, t.val + c, L, AA);
            }
        }
        L.pollLast();//这个是关键,返回上一层时,这层的这个点该出来了,你不在路径里啦 
    }
}

总有大神的代码更简洁
来自牛客网@Xy。

public class Solution {
    private ArrayList<ArrayList<Integer>> listAll = new ArrayList<ArrayList<Integer>>();
    private ArrayList<Integer> list = new ArrayList<Integer>();

    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        if(root == null) return listAll;

        list.add(root.val);

        target -= root.val;//用target减去节点值
        if(target == 0 && root.left == null && root.right == null)
            //新建一个ArrayList,不然listAll里面存的都是指向同一个list的引用,list的内容一变,listAll里面就全变了
            listAll.add(new ArrayList<Integer>(list));
        FindPath(root.left, target);
        FindPath(root.right, target);

        list.remove(list.size()-1);

        return listAll;
    }
}

注意,这道题不存在剪枝的问题,必须遍历到叶节点再判断和,因为树节点的值可能是负数。
思路二:用栈

//2.1
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        ArrayList<ArrayList<Integer>> aa=new ArrayList<>();
        if(root==null)return aa;

        ArrayList<Integer> a=new ArrayList<>();//动态保存路径节点
        Stack<TreeNode> s=new Stack<>();//用栈来实现带记忆的深度遍历
        Stack<Boolean> flag=new Stack<>();//用作区分栈中节点的不同层次

        s.push(root);      
        flag.push(false);//标记栈是和s栈同步变化的,其实就是s栈中节点所附带的标记

        TreeNode t;
        boolean mark;

        while(!s.isEmpty()){
            t=s.peek();
            mark=flag.peek();//看看栈顶的节点是 还没有把它的子节点压入栈中(false) 还是 它的子节点已经压过但是已经遍历过它们了故它们早已出栈了(true)

            if(mark){//这个点也要出栈
                s.pop();
                flag.pop();//标记也要同步出栈
                target+=t.val;//和值要反向一步,这里与递归是不同的,递归不用管 和值 是因为递归 整型数是值传递
                a.remove(a.size()-1);//保存的这个路径节点也要出栈
                continue;//这个不能掉
            }

            a.add(t.val);
            target-=t.val;
            //将这个点的标记从false换成true
            flag.pop();
            flag.push(true);
            //到叶节点且符合目标和值就将路径装入嵌套容器
            if(t.left==null&&t.right==null&&target==0){
                aa.add(new ArrayList<Integer>(a));
            }
            else{  //否则就将左右子节点入栈,注意它们的标记也要入栈 
                if(t.right!=null){s.push(t.right);flag.push(false);}
                if(t.left!=null){s.push(t.left);flag.push(false);}
            }            
        }      
        return aa;
    }    
}

上面这个2.1 区分栈中节点的不同层次 用的是同样大小的一个栈来做标记,还可以用分隔符在栈里面直接来分隔区分,分隔符就选null空引用吧,见2.2

//2.2 在每个节点有子节点准备进栈时,在这个节点后面加一个空节点分隔符
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        ArrayList<ArrayList<Integer>> aa=new ArrayList<>();
        if(root==null)return aa;

        ArrayList<Integer> a=new ArrayList<>();
        Stack<TreeNode> s=new Stack<>();
        s.push(root);

        TreeNode t;
        TreeNode mark=null;

        while(!s.isEmpty()){
            t=s.peek();

            if(t==null){//遇到空节点说明这个 空节点前面的节点 的子节点都已遍历,弹出这个节点
                s.pop();
                target+=s.peek().val;//值也要同步变化
                s.pop(); 
                a.remove(a.size()-1);//路径也要变化
                continue;
            }
            a.add(t.val);
            target-=t.val;

            if(t.left==null&&t.right==null){//叶节点
                if(target==0){aa.add(new ArrayList<Integer>(a));}
                s.pop();//叶节点后面没有空节点,只需要弹一次
                target+=t.val;
                a.remove(a.size()-1);
            }
            else if(t.left!=null||t.right!=null){
                s.push(mark);//在子节点准备入栈前,插入空节点分隔符
                if(t.right!=null){s.push(t.right);}
                if(t.left!=null){s.push(t.left);}     
            }      
        }       
        return aa;
    }  
}

还有另一种用栈的写法
来自牛客网@Nearby36

#include <iostream>
#include <vector>

using namespace std;

struct TreeNode{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL){}
}

vector<vector<int> > FindPath(TreeNode *root, int expectNumber){
    vector<vector<int> > res;   
    if (root == NULL)
        return res;
    stack<TreeNode *> s;
    int sum = 0; //当前和
    vector<int> curPath; //当前路径
    TreeNode *cur = root; //当前节点
    TreeNode *last = NULL; //保存上一个出栈的节点
    while (cur||!s.empty()){
        if (cur == NULL){
            TreeNode *temp = s.top();
            if (temp->right != NULL && temp->right != last){
                cur = temp->right; //如果右子节点存在并且没有被遍历过,如果遍历过,上一个出栈的last就是这个右子节点
            }else{
                last = temp; //出栈之前保存一下
                s.pop();
                curPath.pop_back(); //从当前路径删除
                sum -= temp->val;
            }  
        }
        else{
            s.push(cur);
            sum += cur->val;
            curPath.push_back(cur->val);
            if (cur->left == NULL && cur->right == NULL && sum == expectNum){
                res.push_back(curPath);
            }
            cur = cur->left; //先序遍历,左子树先于右子树
        }
    }
    return res;
}

7、把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
思路一:最笨的方法,全排列+排序

import java.util.ArrayList;
import java.util.Arrays;
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers.length==0)return "";
        ArrayList<String> a=new ArrayList<>();

        f(numbers,0,a);

        long min=Long.MAX_VALUE;
        for(String ss:a){//找到所有字符串中的最小值
            if(Long.parseLong(ss)<min)min=Long.parseLong(ss);
        }
        return String.valueOf(min);
    }

    void f(int[] s,int k,ArrayList<String> A){
        if(k==s.length-1){
            A.add(Arr(s));
        }
        for(int i=k;i<s.length;i++) {//全排列
            swap(s,k,i);
            f(s,k+1,A);
            swap(s,k,i);

        }
    }

    void swap(int[] s,int k,int i){
        if(k==i)return;
        int temp=s[k];
        s[k]=s[i];
        s[i]=temp;
    }
    //将整型数组转化为字符串
    String Arr(int[] s){
        StringBuffer sb = new StringBuffer();
        for(int i=0;i<s.length;i++){
            sb.append(s[i]);
        }
        return sb.toString();
    }
}

思路二:其实就是重新定义排序的规则,若ab > ba 则 a > b;若ab < ba 则 a < b; 若ab = ba 则 a = b。然后排成从小到大的顺序就是最小数字啦。普通排序和重写比较器都可以。
来自牛客网@此广告位出租

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        String str = "";
        for (int i=0; i<numbers.length; i++){
            for (int j=i+1; j<numbers.length; j++){
                int a = Integer.valueOf(numbers[i]+""+numbers[j]);//先将两个数字拼接成字符串,再将字符串转化成数字
                int b = Integer.valueOf(numbers[j]+""+numbers[i]);
                if (a > b){
                    int t = numbers[i];
                    numbers[i] = numbers[j];
                    numbers[j] = t;
                }                 
            }
        }
        //这样搞没有用sb好
        for (int i = 0; i < numbers.length; i++) {
            str += String.valueOf(numbers[i]);
        }
        return str;
    }
}

来自牛客网@飞dsadsadasd

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers.length==0)return "";

        ArrayList<Integer> list= new ArrayList<Integer>();
        for(int i=0;i<numbers.length;i++){
           list.add(numbers[i]); 
        }
        //重写比较器
        Collections.sort(list, new Comparator<Integer>(){  
           public int compare(Integer str1,Integer str2){
             String s1=str1+""+str2;
             String s2=str2+""+str1;
             return s1.compareTo(s2);
           }
        });

        StringBuffer sb = new StringBuffer();
        for(int i=0;i<list.size();i++){
            sb.append(list.get(i));
        }
        return sb.toString();
    }
}

8、判断字符串是否表示数值

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串”+100”,”5e2”,”-123”,”3.1416”和”-1E-16”都表示数值。 但是”12e”,”1a3.14”,”1.2.3”,”+-5”和”12e+4.3”都不是。
补充一下规则:E前面可以是小数,小数可以写成”.5”和”5.”;单独的正负号和小数点不合法;E后面不可以有小数点。
思路一:这种方法需要很多的标志位

public class Solution {
    public boolean isNumeric(char[] str) {
        boolean eafter=false;//为true表示刚遇见了E
        boolean eexist=false;//为true表示E已经存在
        boolean pointexist=false;//为true表示小数点已经存在
        boolean pointafter=false;//为true表示刚遇到小数点
        boolean pnafter=false;//为true标志刚遇见了正负号
        boolean nexist=false;//为true表示已经有至少一个数字了0-9

        for(int i=0;i<str.length;i++) {
            if (str[i] < '0' || str[i] > '9') {//不是数字
                //以下罗列合法的情况
                if ((str[i] == '+' || str[i] == '-')){如果是正负号
                    if(i==0){//如果是第一位
                        pnafter=true;
                        continue;
                    }
                    if(eafter){//如果是E后面一位
                        pnafter=true;
                        eafter=false;
                        continue;
                    }
                }
                if (str[i]=='E'||str[i]=='e') {//如果是E
                    if(!eexist){//如果之前没有过E
                        eexist=true;
                        eafter=true;
                        continue;
                    }
                }
                if(str[i]=='.'){//如果是小数点
                    if(!pointexist&&!eexist){//之前没有过小数点或者没有出现过E
                        pointafter=true;
                        pointexist=true;
                        continue;
                    }
                }
                return false;//一旦遇到非数字字符,除了以上的情况,都是非法
                }
                else{//遇到数字,更新相应的标志位
                    if(!nexist){
                        nexist=true;
                    }
                    if(pointafter){
                        pointafter=false;
                    }
                    if(pnafter){
                        pnafter=false;
                    }
                    if(eafter){
                        eafter=false;
                    }
            }
         }
        //能遍历完说明中间没有直接被判非法,那就看末尾
        if(eafter||pnafter){return false;}//以E和正负号结尾的,非法
        if(pointafter&&!nexist){return false;}//以小数点结尾的,前面一定得出现过数字,否则非法
        //除此之外,都合法
        return true;
        }
    }

思路二:自动机

public class Solution1 {
    public boolean isNumeric(char[] str) {
        int state=0;
        int index=0;
        boolean nexist=false;//特别为小数点准备的,如果以小数点结尾或者小数点后面接E,小数点的前面一定要有数字
        while(index<str.length){
            switch(state){
                case 0:
                    if(str[index]=='+'||str[index]=='-')state=1;
                    else if(str[index]>='0'&&str[index]<='9'){state=2;nexist=true;}
                    else if(str[index]=='.')state=3;
                    else return false;
                    break;

                case 1:
                    if(str[index]=='.')state=3;
                    else if(str[index]>='0'&&str[index]<='9'){state=2;nexist=true;}
                    else return false;
                    break;

                case 2:
                    if(str[index]>='0'&&str[index]<='9'){state=2;nexist=true;}
                    else if(str[index]=='.')state=3;
                    else if(str[index]=='e'||str[index]=='E')state=5;
                    else return false;
                    break;

                case 3:
                    if(str[index]>='0'&&str[index]<='9'){state=4;nexist=true;}
                    //小数点后面接E,小数点的前面一定要有数字
                    else if((str[index]=='e'||str[index]=='E')&&nexist)state=5;
                    else return false;
                    break;

                case 4:
                    if(str[index]>='0'&&str[index]<='9')state=4;
                    else if(str[index]=='e'||str[index]=='E')state=5;
                    else return false;
                    break;

                case 5:
                    if(str[index]>='0'&&str[index]<='9')state=7;
                    else if(str[index]=='+'||str[index]=='-')state=6;
                    else return false;
                    break;

                case 6:
                    if(str[index]>='0'&&str[index]<='9')state=7;
                    else return false;
                    break;

                case 7:
                    if(str[index]>='0'&&str[index]<='9')state=7;
                    else return false;
                    break;
            }
        index++;
        }
        return state==2||(state==3&&nexist)||state==4||state==7;
    }
}

当状态一多,这样写就太繁琐了
来自牛客网@Bine

class Solution {
public:
    char arr[10] = "+-n.ne+-n";
    int turn[10][9] = {
       //+ - n . n e + - n
        {1, 1, 1, 1, 0, 0, 0, 0, 0},    // # start
        {0, 0, 1, 1, 0, 0, 0, 0, 0},    // +
        {0, 0, 1, 1, 0, 0, 0, 0, 0},    // -
        {0, 0, 1, 1, 0, 1, 0, 0, 0},    // n
        {0, 0, 0, 0, 1, 1, 0, 0, 0},    // .
        {0, 0, 0, 0, 1, 1, 0, 0, 0},    // n
        {0, 0, 0, 0, 0, 0, 1, 1, 1},    // e
        {0, 0, 0, 0, 0, 0, 0, 0, 1},    // +
        {0, 0, 0, 0, 0, 0, 0, 0, 1},    // -
        {0, 0, 0, 0, 0, 0, 0, 0, 1}     // n
    };
    bool isNumeric(char* string) {
        bool nexist=false;
        int cur = 0;//当前元素从cur层出发能不能到目的层
        for(int j, i = 0; string[i]; i++) {
            //对某个字符,从上一层能不能到达现在这个字符所属的那一层
            for(j = 0; j < 9; j++) {
                //只在上一层能到达的层中寻找
                if(turn[cur][j]) {
                    //如果在能到达的层中找到了当前字符的归属层
                    if(('0' <= string[i] && string[i] <= '9' && arr[j] == 'n') ||
                        (string[i] == 'E' && arr[j] == 'e')||
                        string[i] == arr[j]) {
                        cur = j + 1;//更新层数,当前字符属于j+1层,下一个字符就看能不能从j+1层出发的目的层中找到归属
                        if(cur==3)nexist==true;
                        break;
                    }
                }
            }
            if(j == 9) return false;//找完了9个层都没找到,说明不匹配,直接false
        }
        //最后一个字符所在的层
        return cur==3||(cur==4&&nexist)||cur==5||cur==9;
    }
};

思路三:正则表达式
来自牛客网@傻傻傻傻

public class Solution {
    public boolean isNumeric(char[] str) {
        String string = String.valueOf(str);
        return string.matches("[\\+\\-]?\\d*(\\.\\d+)?([eE][\\+\\-]?\\d+)?");
    }
}
/*
以下对正则进行解释:
[\\+\\-]?            -> 正或负符号出现与否
\\d*                 -> 整数部分是否出现,如-.34 或 +3.34均符合
(\\.\\d+)?           -> 如果出现小数点,那么小数点后面必须有数字;
                        否则一起不出现
([eE][\\+\\-]?\\d+)? -> 如果存在指数部分,那么e或E肯定出现,+或-可以不出现,
                        紧接着必须跟着整数;或者整个部分都不出现
*/

这里的规则认为”5.”是不合法的,”.5”是合法的。
附上正则表达式小手册

9、连续一段正整数的和

找出所有和为S的连续正数序列。
思路一:解一元二次方程,由起始值算个数

import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer> > aa=new ArrayList<>();
        if(sum<3)return aa;

        double s=sum;
        for(int i=1;i<s/2;i++){ //i是起始值,起始值肯定不能超过s的一半
            double n=(Math.sqrt(Math.pow(2*i-1,2)+8*s)-2*i-1)/2;//n是算出来的,尾数是i+n
            if(n==(int)n){//如果n后面没有小数点,就找到了
                ArrayList<Integer> a=new ArrayList<>();
                for(int m=i;m<=i+n;m++){
                    a.add(m);
                }
                aa.add(a);
            }
        }
        return aa;
    }
}

思路二:由个数算起始值
来自牛客网@丁满历险记

import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> ans = new ArrayList<>();
        //序列最长可能有多长,假设起点最小,从1开始,S = (1 + n) * n / 2,n最大就是根号2n
        //序列越长,起始点越小,所以存放的顺序也满足了从小到大
        for (int n = (int) Math.sqrt(2 * sum); n >= 2; n--) {
            //长度为奇数,sum除以n是要整除的,商就是中间那个数。长度为偶数,商是几点5,余数就是n个0.5相乘
            if ((n & 1) == 1 && sum % n == 0 || (n & 1) == 0 && sum % n == n/2) {
                ArrayList<Integer> list = new ArrayList<>();
                //由个数算起点,起点是(sum / n) - (n - 1) / 2
                for (int j = 0, k = (sum / n) - (n - 1) / 2; j < n; j++, k++) {
                    list.add(k);
                }
                ans.add(list);
            }
        }
        return ans;
    }
}

思路三:双指针,但是感觉输入的数值大了性能不太好

public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> listAll = new ArrayList<ArrayList<Integer>>();
        if (sum < 3)
            return listAll;
        int small = 1;
        int big = 2;
        int middle = (sum + 1) / 2;
        int curSum = 3;
        while (small < middle) {//起点不超过所要求的和的一半
            while (curSum < sum) {
                big++;
                curSum += big;
            }
            if (curSum == sum) {
                ArrayList<Integer> list = new ArrayList<>();
                for (int i = small; i <= big; i++) {
                    list.add(i);
                }
                listAll.add(list);
            }
            curSum -= small;
            small++;
        }
        return listAll;
    }

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

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
思路一:用Map保存次数,不过有点依赖于容器了

import java.util.HashMap;
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        HashMap<Integer,Integer> h=new HashMap();
        int c,max=0;
        for(int i=0;i<array.length;i++){
            if(h.containsKey(array[i])){
                c=h.get(array[i]);
                h.replace(array[i],c,c+1);//更新次数
                max=max<c+1?c+1:max;//保存次数的最大值
            }
            else{
                h.put(array[i],1);
                max=max<1?1:max;
            }
            if(max>array.length/2){
                return array[i];
            }
        }
        return 0;
    }
}

思想二:阵地攻防战,如果次数为0,当前数就是新参照士兵,遇到相同的,次数++,遇到不同的,次数–。最后留下的那个士兵再去验证它的出现次数,如果次数大于一半,那这个士兵就是那个数;如果次数不大于一半,可能这个士兵并不是出现次数最多的,但其他的士兵肯定也没有过半数,因为如果它过了半数,它一定会最后留下来。

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
      if(array.length<=0){
            return 0;
        }
        int result = array[0];
        int times = 1;

        for(int i=0;i<array.length;i++){
            if(times==0){
                result = array[i];
                times =1;
            }else if(array[i]==result)
                times++;
             else
                times--;
        }
        int time = 0;
        for(int i=0;i<array.length;++i){
            if(array[i] == result)
                time++;
        }
        if(time*2<array.length){
            System.out.println(time);
            return 0;
        }else{
            return result;
        }
    }
}

思路三:先排序,再取中位数,然后验证这个中位数的次数,如果过半数,成了,如果没有过半数,那也不可能有其他数过半数了。但是排序之后只取一个中位数实在是太浪费了,所以可以用TOP-K算法得到中位数,线性时间复杂度。

点赞