sword45

  1. 二叉搜索树与双向链表
题目要求:  
输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表,不能创建任何新  
的节点,只能调整树中节点指针的指向。  

解题思路:  
二叉树的left、right可以看做双向链表的prev、next,因此可以完成二叉搜索树到  
双向链表的转换,关键是如何保证转换后为排序好的链表。  
最终生成的双向链表有序,而二叉搜索树的中序遍历就是有序的,可按照中序遍历的  
思路完成此题。关键思路如下:从根节点开始,让左子树部分的最后一节点,中,右  
子树的第一个节点,这三个节点完成双向链表的重组,然后对于左子树,右子树继续  
上述重组过程,递归完成。  

/*
方法一:非递归版
解题思路:
1.核心是中序遍历的非递归算法。
2.修改当前遍历节点与前一遍历节点的指针指向。
*/
import java.util.Stack;

public class P194_SerializeBinaryTrees {

    public TreeNode ConvertBSTToBiList(TreeNode root) {
        if (root == null)
            return null;
        Stack stack = new Stack();
        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.定位至左子树双链表最后一个节点。
3.如果左子树链表不为空的话,将当前root追加到左子树链表。
4.将右子树构造成双链表,并返回链表头节点。
5.如果右子树链表不为空的话,将该链表追加到root节点之后。
6.根据左子树链表是否为空确定返回的节点。
*/
import java.util.Stack;

public class P194_SerializeBinaryTrees {

    public TreeNode Convert(TreeNode root) {
        if (root == null)
            return null;
        if (root.left == null && root.right == null)
            return root;
        // 1.将左子树构造成双链表,并返回链表头节点
        TreeNode left = Convert(root.left);
        TreeNode p = left;
        // 2.定位至左子树双链表最后一个节点
        while (p != null && p.right != null) {
            p = p.right;
        }
        // 3.如果左子树链表不为空的话,将当前root追加到左子树链表
        if (left != null) {
            p.right = root;
            root.left = p;
        }
        // 4.将右子树构造成双链表,并返回链表头节点
        TreeNode right = Convert(root.right);
        // 5.如果右子树链表不为空的话,将该链表追加到root节点之后
        if (right != null) {
            right.left = root;
            root.right = right;
        }
        return left != null ? left : root;
    }
}

/*
方法三:改进递归版
解题思路:
思路与方法二中的递归版一致,仅对第2点中的定位作了修改,
新增一个全局变量记录左子树的最后一个节点。
*/
// 记录子树链表的最后一个节点,终结点只可能为只含左子树的非叶节点与叶节点
import java.util.Stack;

public class P194_SerializeBinaryTrees {
    protected TreeNode leftLast = null;

    public TreeNode Convert(TreeNode root) {
        if (root == null)
            return null;
        if (root.left == null && root.right == null) {
            leftLast = root;// 最后的一个节点可能为最右侧的叶节点
            return root;
        }
        // 1.将左子树构造成双链表,并返回链表头节点
        TreeNode left = Convert(root.left);
        // 3.如果左子树链表不为空的话,将当前root追加到左子树链表
        if (left != null) {
            leftLast.right = root;
            root.left = leftLast;
        }
        leftLast = root;// 当根节点只含左子树时,则该根节点为最后一个节点
        // 4.将右子树构造成双链表,并返回链表头节点
        TreeNode right = Convert(root.right);
        // 5.如果右子树链表不为空的话,将该链表追加到root节点之后
        if (right != null) {
            right.left = root;
            root.right = right;
        }
        return left != null ? left : root;
    }
}

  1. 序列化二叉树
题目要求:  
实现两个函数,分别用来序列化和反序列化二叉树。  
解题思路:  
此题能让人想到重建二叉树。但二叉树序列化为前序遍历序列和中序遍历序列,  
然后反序列化为二叉树的思路在本题有两个关键缺点:1.全部数据都读取完才  
能进行反序列化。2.该方法需要保证树中节点的值各不相同(本题无法保证)。  
其实,在遍历结果中,记录null指针后(比如用一个特殊字符$),那么任何  
一种遍历方式都能回推出原二叉树。但是如果期望边读取序列化数据,边反序  
列化二叉树,那么仅可以使用前序或层序遍历。但层序记录的null个数要远多  
于前序,因此选择使用记录null指针的前序遍历进行序列化。  

编码就是递归先序遍历  
解码递归逻辑,遇到一个$就是空,遇到数字就是一个结点,左子树递归剩下的序列  
左子树返回之后,右子树递归剩下的序列  
node.left = deserializeCore(stringBuilder);  
node.right = deserializeCore(stringBuilder);  

package chapter4;
import structure.TreeNode;


public class P194_SerializeBinaryTrees {
    //为空的位置用$表示,用递归先序遍历  
    public static String serialize(TreeNode root){
        if(root==null)
            return "$,";
        StringBuilder result = new StringBuilder();
        result.append(root.val);
        result.append(",");
        result.append(serialize(root.left));
        result.append(serialize(root.right));
        return result.toString();
    }
    public static TreeNode deserialize(String str){
        StringBuilder stringBuilder = new StringBuilder(str);
        return deserializeCore(stringBuilder);
    }
    public static TreeNode deserializeCore(StringBuilder stringBuilder){
        if(stringBuilder.length()==0)
            return null;
        //获取到第一个字符,之后删除第一个字符
        String num = stringBuilder.substring(0,stringBuilder.indexOf(","));
        stringBuilder.delete(0,stringBuilder.indexOf(","));
        //删除逗号
        stringBuilder.deleteCharAt(0);
        //递归出口
        if(num.equals("$"))
            return null;
        TreeNode node = new TreeNode<>(Integer.parseInt(num));
        //分别递归
        node.left = deserializeCore(stringBuilder);
        node.right = deserializeCore(stringBuilder);
        return node;
    }
    public static void main(String[] args){
        //            1
        //          /   \
        //         2     3
        //       /      / \
        //      4      5   6
        //    1,2,4,$,$,$,3,5,$,$,6,$,$
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.right.left = new TreeNode(5);
        root.right.right = new TreeNode(6);
        System.out.println("原始树:"+root);
        String result = serialize(root);
        System.out.println("序列化结果:"+result);
        TreeNode deserializeRoot = deserialize(result);
        System.out.println("反序列后的树:"+deserializeRoot);
    }
}
  1. 字符串的排列
题目要求:  
输入一个字符串,打印出该字符串中字符的所有排列。如输入abc,则打印  
abc,acb,bac,bca,cab,cba。  

解题思路:  
排列与组合是数学上的常见问题。解题思路与数学上求排列总数类似:首先确定  
第一个位置的元素,然后一次确定每一个位置,每个位置确实时把所有情况罗列  
完全即可。以abc为例,我之前更习惯于设置三个空,然后选择abc中的元素放入  
上述的空中。而书中给的思路是通过交换得到各种可能的排列,具体思路如下:  


对于a,b,c(下标为0,1,2)  
0与0交换,得a,b,c => 1与1交换,得a,b,c =>2与2交换,得a,b,c(存入)
                => 1与2交换,得a,c,b =>2与2交换,得a,c.b(存入)
0与1交换,得b,a,c => 1与1交换,得b,a,c =>2与2交换,得b,a,c(存入)
                => 1与2交换,得b,c,a =>2与2交换,得b,c,a(存入)
0与2交换,得c,b,a => 1与1交换,得c,b,a =>2与2交换,得c,b,a(存入)
                => 1与2交换,得c,a,b =>2与2交换,得c,a.b(存入)  


书中解法并未考虑有字符重复的问题。对于有重复字符的情况如a,a,b,交换  
0,1号元素前后是没有变化的,即从生成的序列结果上看,是同一种排列,  
因此对于重复字符,不进行交换即可,思路如下:  


对于a,a,b(下标为0,1,2)
0与0交换,得a,a,b => 1与1交换,得a,a,b =>2与2交换,得a,a,b(存入)
                => 1与2交换,得a,b,a =>2与2交换,得a,b,a(存入)
0与1相同,跳过
0与2交换,得b,a,a =>1与1交换,得b,a,a =>2与2交换,得b,a,a(存入)
                =>1与2相同,跳过


考虑了字符重复的解法的实现如下  

package chapter4;
import java.util.*;


public class P197_StringPermutation {
    public static List permutation(char[] strs) {
        if (strs == null || strs.length == 0)
            return null;
        List # ret = new LinkedList<>();
        permutationCore(strs, ret, 0);
        return ret;
    }
    //下标为bound的字符依次与[bound,length)的字符交换,如果相同不交换,直到最后一个元素为止。
    //如a,b,c
    //0与0交换,得a,b,c => 1与1交换,得a,b,c =>2与2交换,得a,b,c(存入)
    //                => 1与2交换,得a,c,b =>2与2交换,得a,c.b(存入)
    //0与1交换,得b,a,c => 1与1交换,得b,a,c =>2与2交换,得b,a,c(存入)
    //                => 1与2交换,得b,c,a =>2与2交换,得b,c,a(存入)
    //0与2交换,得c,b,a => 1与1交换,得c,b,a =>2与2交换,得c,b,a(存入)
    //                => 1与2交换,得c,a,b =>2与2交换,得c,a.b(存入)

    //如a,a,b
    //0与0交换,得a,a,b => 1与1交换,得a,a,b =>2与2交换,得a,a,b(存入)
    //                => 1与2交换,得a,b,a =>2与2交换,得a,b,a(存入)
    //0与1相同,跳过
    //0与2交换,得b,a,a =>2与2交换,得b,a,a(存入)
    public static void permutationCore(char[] strs, List  ret, int bound) {

        //bound从0开始,到达3的时候,添加结果
        //Arrays.copyOf(strs, strs.length)相当于新建对象
        //否则由于引用一个对象,ret中的元素都会一样,例子见下面
        if (bound == strs.length)
            ret.add(Arrays.copyOf(strs, strs.length));
        Set set = new HashSet<>();

        //需要交换的次数length-bound,分别需要交换3,2,1次
        for (int i = bound; i < strs.length; i++) {
            //当前i指向的元素与递归树横向的不重复,重复的放弃向下递归,如0与1相同
            if (set.add(strs[i])) {
                swap(strs, bound, i);
                permutationCore(strs, ret, bound + 1);
                swap(strs, bound, i);
            }
        }
    }

    public static void swap(char[] strs, int x, int y) {
        char temp = strs[x];
        strs[x] = strs[y];
        strs[y] = temp;
    }

    public static void main(String[] args) {
        char[] strs = {'a', 'b', 'c'};
        List # ret = permutation(strs);
        for (char[] item : ret) {
            for (int i = 0; i < item.length; i++)
                System.out.print(item[i]);
            System.out.println();
        }
        System.out.println();
        char[] strs2 = {'a', 'a', 'b','b'};
        List # ret2 = permutation(strs2);
        for (char[] item : ret2) {
            for (int i = 0; i < item.length; i++)
                System.out.print(item[i]);
            System.out.println();
        }
    }
}


//上面的Arrays.copyOf例子
import java.util.LinkedList;
import java.util.List;

public class CopyOfTest {

    public static void swap(char[] strs, int x, int y) {
        char temp = strs[x];
        strs[x] = strs[y];
        strs[y] = temp;
    }

    public static void main(String[] args) {
        char[] strs1 = { 'a', 'b', 'c' };
        List # ret = new LinkedList<>();
        // ret.add(Arrays.copyOf(strs, strs.length));
        ret.add(strs1);
        swap(strs1, 0, 1);
        ret.add(strs1);
        for (char[] cs : ret) {
            System.out.println(cs);
        }
    }

}


/*
迭代算法:字典生成算法
从后向前找前一项比后一项小的,seq[p] < seq[p+1]  
从q(p+1)向后找最后一个比seq[p]大的数
交换这两个位置上的值
将p之后的序列倒序排列
这就是输出的一个序列
*/
从q(p+1)向后找最后一个比seq[p]大的数

package algorithm;
import java.util.ArrayList;
import java.util.Arrays;

public class PermutationTest {

    public ArrayList permutation(String str) {
        ArrayList res = new ArrayList<>();

        if (str != null && str.length() > 0) {
            char[] seq = str.toCharArray();
            Arrays.sort(seq); // 排列
            res.add(String.valueOf(seq)); // 先输出一个从小到大的排列

            int len = seq.length;
            while (true) {
                int p = len - 2, q;
                //从后向前找前一项比后一项小的,seq[p] < seq[p+1]
                //p现在找到位置是前一项               
                while (p >= 0 && seq[p] >= seq[p + 1])
                    --p;
                if (p == -1)
                    break; // 是dcba这种顺序,就退出

                //q为了从p向后找最后一个比seq[p]大的数
                q = p + 1;
                //从q(p+1)向后找最后一个比seq[p]大的数,就是找到第一个小或者等于的位置再减一
                while (q < len && seq[q] > seq[p])
                    q++;
                --q;
                // 交换这两个位置上的值
                swap(seq, q, p);
                // 将p之后的序列倒序排列
                reverse(seq, p + 1);
                res.add(String.valueOf(seq));
            }
        }

        return res;
    }

    public static void reverse(char[] seq, int start) {
        int len;
        if (seq == null || (len = seq.length) <= start)
            return;
        //i最大是(len-start)/2向下取整,从中轴进行反转
        for (int i = 0; i < ((len - start) >> 1); i++) {
            int p = start + i, q = len - 1 - i;
            if (p != q)
                swap(seq, p, q);
        }
    }

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

    public static void main(String[] args) {
        ArrayList list = permutation("abc");
        System.out.println(list);
    }
}

38.2 字符串的组合

题目要求:
输入一个字符串,打印出该字符串中字符的所有组合。如输入abc,  
则打印a,b,c,ab,ac,bc,abc。  

解题思路:  
这道题目是在38题.字符串的排列的扩展部分出现的。排列的关键在于次序,而组合  
的关键在于状态,即该字符是否被选中进入组合中。  
对于无重复字符的情况,以a,b,c为例,每一个字符都有两种状态:被选中、不被选中;  
2*2*2=8,再排除为空的情况,一共有7种组合:  


a(被选中)b(被选中)c(被选中)            =>    abc
a(被选中)b(被选中)c(不被选中)          =>    ab
a(被选中)b(不被选中)c(被选中)          =>    ac
a(被选中)b(不被选中)c(不被选中)        =>    a
a(不被选中)b(被选中)c(被选中)          =>    bc
a(不被选中)b(被选中)c(不被选中)        =>    b
a(不被选中)b(不被选中)c(被选中)        =>    c
a(不被选中)b(不被选中)c(不被选中)      =>    空(不作为一种组合情况)


对于有重复字符的情况,不重复的字符各有两种状态;而重复的字符状态个数与  
重复次数有关。以a,a,b为例,b有两种状态:被选中、不被选中,a,a有三种状态:  
被选中2个,被选中1个,不被选中。2*3=6,排除为空的情况,一共有5种组合:  


a(被选中2个)b(被选中)                 =>    aab
a(被选中2个)b(不被选中)               =>    aa
a(被选中1个)b(被选中)                 =>    ab
a(被选中1个)b(不被选中)               =>    a
a(不被选中)b(被选中)                  =>    b
a(不被选中)b(不被选中)                =>    空(不作为一种组合情况)


上述无重复字符的情况可以看作是有重复字符的情况的特例,因此,仅实现  
有重复字符的字符串组合处理思路即可。  

package chapter4;
import java.util.*;

public class P199_StringCombination {
    //无重复字符:对于每一个字符,都由两种选择:被选中、不被选中;
    //有重复字符:整体需要先排序,对于重复n遍的某种字符,有如下选择:不被选中,选1个,选2个...选n个。
    public static List combination(char[] strs) {
        if (strs == null || strs.length == 0)
            return null;
        Arrays.sort(strs);//排序过的字符数组
        List # ret = new LinkedList<>();
        combinationCore(strs,ret,new StringBuilder(),0);
        return ret;
    }
    public static void combinationCore(char[] strs,List # ret,StringBuilder stringBuilder,int cur){
        //递归出口到达最后一个字符后面加一的位置
        if(cur==strs.length ) {
            if(stringBuilder.length()>0)
                ret.add(stringBuilder.toString().toCharArray());
        }
        //到达最后一个字符,或者当前字符和后面一个字符不同,由于排过序说明当前字符没重复
        else if(cur+1==strs.length||strs[cur]!=strs[cur+1]){
            combinationCore(strs,ret,stringBuilder.append(strs[cur]),cur+1);
            stringBuilder.deleteCharAt(stringBuilder.length()-1);
            combinationCore(strs,ret,stringBuilder,cur+1);

        }
        else{
            //dumplicateStart记录重复字符开始位置,从这个位置开始向后查找,相同的都放到stringBuilder
            //cur直到指到不重复的字符,或者到length也就是最后一个的后面
            int dumplicateStart = cur;
            while(cur!=strs.length&&strs[dumplicateStart]==strs[cur]){
                stringBuilder.append(strs[cur]);
                cur++;
            }
            //newStart指向cur
            int newStart = cur;
            //循环加递归的形式,递归树有多个横向结点,a(被选中2个),a(被选中1个),a(不被选中)
            while (cur>=dumplicateStart) {
                combinationCore(strs, ret, stringBuilder, newStart);
                if(cur!=dumplicateStart)
                    stringBuilder.deleteCharAt(stringBuilder.length() - 1);
                cur--;
            }

        }
    }
    public static void main(String[] args) {
        char[] strs2 = {'a', 'a', 'b'};
        List # ret2 = combination(strs2);
        for (char[] item : ret2) {
            for (int i = 0; i < item.length; i++)
                System.out.print(item[i]);
            System.out.println();
        }
    }
}
#include 
#include 
class Solution {
public:
    std::vector > subsets(std::vector& nums) {
        std::vector > result;
        int all_set = 1 << nums.size();                 //一共的子集数,2的size次方
        for (int i = 0; i < all_set; i++){              //遍历所有子集,一个子集是一个item
            std::vector item;
            for (int j = 0; j < nums.size(); j++){      //遍历判断元素是否应该出现在当前子集
                if (i & (1 << j)){                      //i & (1 << j)判断第j位置的元素是否在第i个item
                    item.push_back(nums[j]);            //相当于把二进制表示的i转成数组
                }
            }
            result.push_back(item);
        }
        return result;
    }
};

int main(){
    std::vector nums;
    nums.push_back(1);
    nums.push_back(2);
    nums.push_back(3);
    std::vector > result;
    Solution solve;
    result = solve.subsets(nums);
    for (int i = 0; i < result.size(); i++){
        if (result[i].size() == 0){
            printf("[]");
        }
        for (int j = 0; j < result[i].size(); j++){
            printf("[%d]", result[i][j]);
        }
        printf("\n");
    }
    return 0;
}
  1. 数组中出现次数超过一半的数字
题目要求:  
找出数组中出现次数超过数组长度一半的数字。如输入{1,2,3,2,2,2,5,4,2},则输出2。  

解题思路:  
因为该数字的出现次数超过了数组长度的一半,因此可以将问题转化为求数组的中位数。  
如果按照这个思路,有以下两种方式解决:排序后求中位数、用快排的分区函数求中位数  
(topK问题),这两种思路都比较简单,此处不再赘述。  
书中还提到一种思路,相当巧妙,可以看作一种特殊的缓存机制。该思路需要一个整型的  
value变量和一个整型的count变量,记录缓存值与该缓存值被命中的次数。缓存规则以及  
执行步骤如下:  


步骤1: 缓存值value,命中次数count均初始化为0。  
步骤2: 从头到尾依次读取数组中的元素,判断该元素是否等于缓存值:  
     步骤2.1:如果该元素等于缓存值,则命中次数加一。  
     步骤2.2:如果该元素不等于缓存值,判断命中次数是否大于1:  
              步骤2.2.1:如果命中次数大于1,将命中次数减去1。  
              步骤2.2.2:如果命中次数小于等于1,则令缓存值等于元素值,命中次数设为1  
步骤3: 最终的缓存值value即为数组中出现次数超过一半的数字。  
此方法时间复杂度o(n),空间复杂度o(1),实现简单。  
package chapter5;


public class P205_MoreThanHalfNumber {
    //转化为topK问题(此处求第k小的值),使用快排的分区函数解决,求第 targetIndex +1小的数字(下标为targetIndex)
    //书中说这种方法的时间复杂度为o(n),但没懂为什么。网上也有人说为o(nlogk)
    public static int moreThanHalfNum1(int[] data){
        if(data==null || data.length==0)
            return 0;
        int left = 0,right=data.length-1;
        //获取执行分区后下标为k的数据值(即第k+1小的数字)
        //无符号右移,空位补0
        int k = data.length>>>1;
        int index = partition(data,left,right);
        while(index!=k){
            if(index>k)
                index = partition(data,left,index-1);
            else
                index = partition(data,index+1,right);
        }
        return data[k];
    }
    //分区,[小,povot,大]
    public static int partition(int[] data,int left,int right){
        int pivot = data[left];
        while(left=pivot)
                right--;
            if(left

40:最小的k个数

题目要求:  
找出n个整数中最小的k个数。例如输入4,5,1,6,2,7,3,8,则最小的4个数字是1,2,3,4。  

解题思路:  
经典的topK问题,网上有很多种思路,在此仅介绍我能想到的几种:  


解法          介绍                            时间       空间   是否修改原数组
1   排序后,前k个即为所求                      o(nlogn)   o(1)      是
2   执行k次直接选择排序                        o(n*k)     o(1)      是
3   使用快排的分区函数求出第k小的元素           o(n)       o(1)      是
4   维护一个长度为k的升序数组,用二分法更新元素  o(nlogk)   o(k)      否
5   创建并维护一个长度为k的最大堆               o(nlogk)   o(k)      否

package chapter5;

public class P209_KLeastNumbers {
    //选择排序,时间复杂度o(N*k),适合k较小的情况
    public static int getLeastNumbers1(int[] data,int k){
        if(data==null||data.length==0||k>data.length)
            return 0;
        for(int i=0;idata.length)
            return 0;
        int left=0,right=data.length-1;
        int index = partition(data,left,right);
        //第k小,也就是序号k-1的元素
        while(index!=k-1){
            if(index=pivot)
                right--;
            if(left data.length)
            return 0;
        // 最大堆,长度需k
        int[] heap = new int[k];
        int i = 0;
        while (i < k) {
            heap[i] = data[i];
            i++;
        }
        // 初始化最大堆
        buildMaxHeap(heap);
        // 遍历data,如果比堆顶小,说明应该进堆,堆顶出去
        while (i < data.length) {
            if (data[i] < heap[0]) {
                heap[0] = data[i];
                adjustMaxHeap(heap, 0);
            }
            i++;
        }
        // 长度为k的最大堆中下标为1的元素就是data数组中第k小的数据值
        return heap[0];
    }

    // 需要从2到0,调整一半的元素,并递归
    public static void buildMaxHeap(int[] heap) {
        for (int i = (heap.length >>> 1) - 1; i >= 0; i--)
            adjustMaxHeap(heap, i);
    }

    // 调整最大堆,i为待调整的下标,i和左右孩子哪个大哪个就上去
    public static void adjustMaxHeap(int[] heap, int i) {
        int left = 2 * i + 1, right = left + 1;
        int max = i;
        if (left < heap.length && heap[left] > heap[max])
            max = left;
        if (right < heap.length && heap[right] > heap[max])
            max = right;
        if (max != i) {
            int temp = heap[i];
            heap[i] = heap[max];
            heap[max] = temp;
            adjustMaxHeap(heap, max);
        }
    }

    public static void main(String[] args){
        int[] data1 = {6,1,3,5,4,2};
        System.out.println(getLeastNumbers1(data1,5));
        int[] data2 = {6,1,3,5,4,2};
        System.out.println(getLeastNumbers2(data2,5));
        int[] data3 = {6,1,3,5,4,2};
        System.out.println(getLeastNumbers3(data3,5));
    }
}

优先级队列的实现

package algorithm;
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Comparator;

public class Solution {

    public ArrayList GetLeastNumbers_Solution(int[] input, int k) {
        ArrayList result = new ArrayList();
        int length = input.length;
        if (k > length || k == 0) {
            return result;
        }
        PriorityQueue maxHeap = new PriorityQueue(k, new Comparator() {

            @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;
                maxHeap.offer(input[i]);
            }
        }
        for (Integer integer : maxHeap) {
            result.add(integer);
        }
        return result;
    }
    
    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] data3 = { 6, 1, 3, 5, 4, 2 };
        ArrayList res = solution.GetLeastNumbers_Solution(data3, 5);
        for (Integer integer : res) {
            System.out.println(integer);
        }

    }
}
  1. 数据流中的中位数
题目要求:  
得到一个数据流中的中位数。  

解题思路:  
此题的关键在于“数据流”,即数字不是一次性给出,解题的关键在于重新写一个结构  
记录历史数据。下面给出三种思路,分别借助于链表、堆、二叉搜索树完成。  

思路1:使用未排序的链表存储数据,使用快排的分区函数求中位数;也可以在插入时  
进行排序,这样中位数的获取会很容易,但插入费时。  

思路2:使用堆存储,两个堆能够完成最大堆-最小堆这样的简单分区,两个堆的数字个  
数相同或最大堆数字个数比最小堆数字个数大1,中位数为两个堆堆顶的均值或者最大堆的堆顶。  

思路3:使用二叉搜索树存储,每个树节点添加一个表征子树节点数目的字段用于找到中位数。  

package algorithm;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class P214_StreamMedian {
    public static interface MedianFinder{
        //模拟读取数据流的过程
        void addNum(double num);
        double findMedian();
    }
    //使用未排序的链表存储数据,使用快排的分区函数求中位数;
    //也可以在插入时进行排序,这样中位数的获取会很容易,但插入费时。
    public static class MedianFinder1 implements MedianFinder{
        List list = null;
        public MedianFinder1() {
            list = new LinkedList<>();
        }
        @Override
        public void addNum(double num) {
            list.add(num);
        }
        @Override
        public double findMedian() {
            if(list.size()==0)
                return 0;
            //如果长度为奇数,求中间的那个数;如果为偶数,求中间两个数的均值
            if((list.size()&1)==1)
                return findKth(list.size()>>>1);
            else
                return (findKth(list.size()>>>1)+findKth((list.size()>>>1)-1))/2;
        }
        //使用快排分区,求第k小的数据值
        private double findKth(int k){
            int start=0,end=list.size()-1;
            int index = partition(start,end);
            while (index!=k){
                if(index=end)
                return end;
            double pivot = list.get(start);
            //[start,bound)小于等于pivot,bound值为pivot,(bound,end]大于pivot
            int bound = start;
            for(int i=start+1;i<=end;i++){
                if(list.get(i)<=pivot){
                    list.set(bound,list.get(i));
                    bound++;
                    list.set(i,list.get(bound));
                }
            }
            list.set(bound,pivot);
            return bound;
        }
    }

    //思路二:使用堆存储
    //两个堆能够完成最大堆-最小堆这样的简单分区,两个堆的数字个数相同或heapMax比heapMin大1
    //中位数为最大堆的堆顶或者两个堆堆顶的均值
    public static class MedianFinder2 implements MedianFinder{
        List maxHeap = null;
        List minHeap = null;
        public MedianFinder2() {
            maxHeap = new ArrayList<>();
            minHeap = new ArrayList<>();
            //0是一个空位不用,从位置1开始
            maxHeap.add(0.0);
            minHeap.add(0.0);
        }
        @Override
        public void addNum(double num) {
            //两个堆数量相同
            if(maxHeap.size()==minHeap.size()){
                //最小堆还没有元素,或者加入元素比最小堆堆顶小,加到最大堆
                if(minHeap.size()<=1||num<=minHeap.get(1))
                    addItemToMaxHeap(num);
                //加入元素比最小堆堆顶大,说明加入元素是最大的,要放到最小堆
                //最小堆堆顶放到最大堆,替换最小堆堆顶为添加元素
                else{
                    addItemToMaxHeap(minHeap.get(1));
                    minHeap.set(1,num);
                    adjustMinHeap(1);
                }
            }
            //heapMax比heapMin大1
            else{
                //比最大堆的堆顶大,直接放到最小堆,heapMax和heapMin元素数相同
                if(num>=maxHeap.get(1))
                    addItemToMinHeap(num);
                //否则最大堆堆顶放到最小堆,替换最大堆堆顶为添加元素
                else{
                    addItemToMinHeap(maxHeap.get(1));
                    maxHeap.set(1,num);
                    adjustMaxHeap(1);
                }

            }
        }
        private void addItemToMaxHeap(double value){
            maxHeap.add(value);
            //curIndex当前指向最后一个元素的位置
            int curIndex = maxHeap.size()-1;
            //当前位置大于1,且当前元素大于其父节点,就是curIndex/2位置的值
            //就把当前结点和父节点交换,然后当前变成curIndex/2位置,保证还是最大值堆
            while (curIndex>1 && maxHeap.get(curIndex)>maxHeap.get(curIndex>>>1)){
                double temp = maxHeap.get(curIndex);
                maxHeap.set(curIndex,maxHeap.get(curIndex>>>1));
                maxHeap.set(curIndex>>>1,temp);
                curIndex = curIndex>>>1;
            }
        }
        private void adjustMaxHeap(int index){
            //父结点和左右子结点找最大,放到上面
            //递归出口就是left和right都大于等于size
            int left = index*2,right=left+1;
            int max = index;
            if(leftmaxHeap.get(max))
                max = left;
            if(rightmaxHeap.get(max))
                max = right;
            if(max!=index){
                double temp = maxHeap.get(index);
                maxHeap.set(index,maxHeap.get(max));
                maxHeap.set(max,temp);
                //递归max的位置
                adjustMaxHeap(max);
            }
        }
        private void addItemToMinHeap(double value){
            minHeap.add(value);
            int curIndex = maxHeap.size()-1;
            while (curIndex>1 && minHeap.get(curIndex)>>1)){
                double temp = minHeap.get(curIndex);
                minHeap.set(curIndex,minHeap.get(curIndex>>>1));
                minHeap.set(curIndex>>>1,temp);
                curIndex = curIndex>>>1;
            }
        }
        private void adjustMinHeap(int index){
            int left = index*2,right=left+1;
            int min = index;
            if(leftminHeap.size())
                return maxHeap.get(1);
            else
                return (maxHeap.get(1)+minHeap.get(1))/2;
        }
    }

    //思路三:使用二叉搜索树存储,每个树节点添加一个表征子树节点数目的字段用于计算中位数。
    public static class TreeNodeWithNums {
        public T val;
        public int nodes;
        public TreeNodeWithNums left;
        public TreeNodeWithNums right;
        public TreeNodeWithNums(T val){
            this.val = val;
            this.nodes = 1;
            this.left = null;
            this.right = null;
        }
    }

    public static void main(String[] args){
        MedianFinder medianFinder1 = new MedianFinder1();
        medianFinder1.addNum(2);
        medianFinder1.addNum(1);
        System.out.println(medianFinder1.findMedian());
        MedianFinder medianFinder2 = new MedianFinder2();
        medianFinder2.addNum(2);
        medianFinder2.addNum(1);
        System.out.println(medianFinder2.findMedian());

        medianFinder1.addNum(3);
        medianFinder2.addNum(3);
        medianFinder1.addNum(4);
        medianFinder2.addNum(4);
        System.out.println(medianFinder1.findMedian());
        System.out.println(medianFinder2.findMedian());
    }
}
  1. 连续子数组的最大和
题目要求:  
输入一个整形数组,数组里有正数也有负数。数组中一个或连续多个整数组成一个子数组。  
求所有子数组的和的最大值,要求时间复杂度为o(n)。  

解题思路:  
暴力求解,简单直接,但时间复杂度o(n^2)。  
其实这种最值问题,很容易让人想到动态规划。对于数据data[],申请一个数组dp[],  
定义dp[i]表示以data[i]为末尾元素的子数组和的最大值。dp的初始化及递推公式可表示为  


dp[i] =  data[i]            i=0或dp[i-1]<=0
         dp[i-1]+data[i]    i!=0且dp[i-1]>0


由于dp[i]仅与dp的前一个状态有关,即在计算dp[i]时dp[i-2],dp[i-3]......,dp[0]  
对于dp[i]没有影响,因此可以省去dp数组,用一个变量记录当前dp值,用另一个变量maxdp  
记录出现的最大的dp值。如此处理后,时间复杂度为o(n),空间复杂度为o(1)。  

package chapter5;


public class P218_GreatestSumOfSubarrays {
    //动态规划,递归公式:dp[i] =  data[i]          i=0或dp[i-1]<=0
    //                           dp[i-1]+data[i]  i!=0且dp[i-1]>0
    //由于只需知道前一个情况的dp值,因此可省去dp数组,申请个变量记录即可
    public static int findGreatestSumOfSumArrays(int[] data){
        if(data==null || data.length==0)
            return 0;
        //dp[i]用于计算以data[i]为结尾元素的连续数组的最大和
        //maxdp用于记录在上述过程中的最大的dp值
        int dp = data[0],maxdp = dp;
        for(int i=1;i0)
                dp += data[i];
            else
                dp = data[i];
            if(dp>maxdp)
                maxdp = dp;
        }
        return maxdp;
    }
    public static void main(String[] args){
        int[] data = {1,-2,3,10,-4,7,2,-5};
        System.out.println(findGreatestSumOfSumArrays(data));
    }
}
  1. 1~n整数中1出现的次数
题目要求:  
输入一个整数,求1~n这n个整数中1出现的次数。如输入12,则包含1的数字有1,10,11,12,  
一共出现了5次1,因此输入5。  

解题思路:  
思路1:通过遍历1~n,然后依次判断每个数字包含1的个数。  
思路2:通过规律递归计算出结果:  

以21345为例。  
步骤1:把1~21345的所有数字分成两段:第1段是1~1345,第2段是1346~21345。  
步骤2:计算第2段中1出现的次数。  
      步骤2.1:计算最高位万位中1出现的次数,要分最高位是否为1考虑。  
              此处最高位大于1,countFirst1 =10^4。  
      步骤2.2:计算其他位中1出现的次数countOhters1=2*10^3*4。  
             (1346~21345与1~20000的countOhters1是相等的,所以可以转化为分析1~20000)  
步骤3:依据步骤1,2,递归计算1~1345。  
代码实现如下:  

package chapter5;
public class P221_NumberOf1 {
    public static int numberOf1Between1AndN(int n){
        int count = 0;
        if(n<=0)
            return count;
        for(int i=1;i<=n;i++)
            count+=numberOf1(i);
        return count;
    }
    private static int numberOf1(int i){
        int count = 0;
        while (i!=0){
            if(i%10==1)
                count++;
            i/=10;
        }
        return count;
    }
    public static int numberOf1Between1AndN2(int n){
        if(n<=0)
            return 0;
        if(n<10)
            return 1;
        String nString = Integer.toString(n);
        char firstChar = nString.charAt(0);
        String apartFirstString = nString.substring(1);
        //计算other~n中1出现的次数,递归计算apartFirstString
        int countFirst1 = 0;
        if(firstChar>'1')
            countFirst1 = power10(nString.length()-1);
        else
            countFirst1 = Integer.parseInt(apartFirstString)+1;
        int countOhters1 = (firstChar-'0')*power10(nString.length()-2)*(nString.length()-1);
        return countFirst1+countOhters1+numberOf1Between1AndN(Integer.parseInt(apartFirstString));
    }
    public static int power10(int n){
        int result = 1;
        for(int i=0;i

44:数字序列中某一位的数字

题目要求:
数字以01234567891011121314...的格式排列。在这个序列中,第5位(从0开始计)是5,  
第13位是1,第19位是4。求任意第n为对应的数字。  

解题思路:
与43题类似,都是数学规律题。如果用遍历的方式,思路代码都很简单,但速度较慢。更好的  
方式是借助于数字序列的规律,感觉更像是数学题。步骤大致可以分为如下三部分:  


以第15位数字2为例(2隶属与12,两位数,位于12从左侧以0号开始下标为1的位置)  
步骤1:首先确定该数字是属于几位数的;
      如果是一位数,n<9;如果是两位数,n<9+90*2=189;
      说明是两位数。
步骤2:确定该数字属于哪个数。10+(15-10)/2= 12。
步骤3:确定是该数中哪一位。15-10-(12-10)*2 = 1, 所以位于“12”的下标为1的位置,即数字2。

以第1001位数字7为例
步骤1:首先确定该数字是属于几位数的;
      如果是一位数,n<9;如果是两位数,n<9+90*2=189;如果是三位数,n<189+900*3=2889;
      说明是三位数。
步骤2:确定该数字属于哪个数。100+(1001-190)/3= 370。
步骤3:确定是该数中哪一位。1001-190-(370-100)*3 = 1,所以位于“370”的下标为1的位置,即数字1。
package chapter5;

public class P225_DigitsInSequence {
    public static int digitAtIndex(int index){
        if(index<0)
            return -1;
        if(index<10)
            return index;
        int curIndex = 10,length = 2;
        int boundNum = 10;
        while (curIndex+lengthSum(length)
  1. 把数组排列成最小的数
题目要求:  
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,使其为所有可能的拼接  
结果中最小的一个。例如输入{3,32,321},则输入321323。  

解题思路:  
此题需要对数组进行排序,关键点在于排序的规则需要重新定义。我们重新定义“大于”,  
“小于”,“等于”。如果a,b组成的数字ab的值大于ba,则称a"大于"b,小于与等于类似。  
比如3与32,因为332大于323,因此我们称3“大于”32。我们按照上述的“大于”,“小于”  
规则进行升序排列,即可得到题目要求的答案。  

package chapter5;

import java.util.Arrays;
import java.util.Comparator;


public class P227_SortArrayForMinNumber {
    public static void printMinNumber(int[] data){
        if(data==null||data.length==0)
            return;
        for(int i=0;i=b return true
    public static boolean bigger(int a,int b){
        String temp1 = a+""+b;
        String temp2 = b+""+a;
        if(temp1.compareTo(temp2)>0)
            return true;
        else
            return false;
    }
    public static void main(String[] args){
        printMinNumber(new int[]{3,32,321});
    }
}

你可能感兴趣的:(sword45)