- 二叉搜索树与双向链表
题目要求:
输入一颗二叉搜索树,将该二叉搜索树转换成一个排序的双向链表,不能创建任何新
的节点,只能调整树中节点指针的指向。
解题思路:
二叉树的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.全部数据都读取完才
能进行反序列化。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);
}
}
- 字符串的排列
题目要求:
输入一个字符串,打印出该字符串中字符的所有排列。如输入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,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:使用未排序的链表存储数据,使用快排的分区函数求中位数;也可以在插入时
进行排序,这样中位数的获取会很容易,但插入费时。
思路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());
}
}
- 连续子数组的最大和
题目要求:
输入一个整形数组,数组里有正数也有负数。数组中一个或连续多个整数组成一个子数组。
求所有子数组的和的最大值,要求时间复杂度为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~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)
- 把数组排列成最小的数
题目要求:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,使其为所有可能的拼接
结果中最小的一个。例如输入{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});
}
}