题目31:连续子数组的最大和
在数组中找一个连续子数组,使它的和最大。
我的方法:利用一个local维护局部子数组最大和,一个global维护全局子数组最大和。因为子数组要连续,local[i]只能出现两种情况:加上当前a[i],即local[i] = local[i-1]+a[i];重新开始,即local[i] = a[i]。我们取较大的作为局部最大和。global[i]的取值也有两种情况:原来的最大值,即global[i-1];当前局部最大值local[i]。我们取较大的作为全局最大和。
public int find2(int[] a)throws Exception{
if(a.length == 0 || a == null){
throw new IllegalArgumentException("输入数组不合法");
}
int local = 0;
int global = Integer.MIN_VALUE;
for(int i=0; i<a.length; i++){
local = Math.max(local+a[i], a[i]);
global = Math.max(global, local);
}
return global;
}
书中方法:假设当前正在维护的最大子数组的和为f[i-1],对于第i个数a[i],如果f[i-1]小于0,那么我们舍弃维护这个连续子数组,以第i个元素为头,重新开始寻找连续子数组,即f[i] = a[i],因为负数会使当前维护的最大子数组和减小,我们肯定重新开始寻找连续子数组;如果f[i-1]大于0,不管a[i]正负,我们加上a[i],即f[i] = f[i-1] + a[i],同时更新全局最大值s[i] = max(f[i], s[i-1])。(如果a[i]为正,没有问题,连续子数组继续增长,如果a[i]为负,a[i]后面的数可能是一个很大的正数,那么就要算上这个a[i],若a[i]后面继续为负数,也有全局最大值记录了f[i-1])
public int find(int[] a) throws Exception{
if(a.length == 0 || a == null){
throw new IllegalArgumentException("输入数组不合法");
}
int curSum = 0;
int result = Integer.MIN_VALUE;
for(int i=0; i<a.length; i++){
//如果前面的和小于0,没有意义,舍去并重新开始
if(curSum < 0){
curSum = a[i];
}else{//如果前面的和大于0,可以继续增加子数组长度
curSum += a[i];
}
//记录下每一步的最大值
if(curSum > result){
result = curSum;
}
}
return result;
}
题目32:从1到n整数中1出现的次数
输入数字n,返回1-n中包含‘1’的数字的个数。
我的方法:遍历所有数字并检查。这种方法的时间复杂度是nlgn。
public int total(int n) throws Exception{
if(n<=0)throw new IllegalArgumentException("输入数字不符合要求");
int sum = 0;
//逐个检查所有数字,数字内包含1就增加总数
for(int i=1; i<=n; i++){
if(isQualified(i)){
sum++;
}
}
return sum;
}
private boolean isQualified(int m){
while(m>0){
if(m%10 == 1){
return true;
}
m /= 10;
}
return false;
}
书中方法:如果我们去寻找数字的特征而不是遍历所有数字并检查那么可以省很多时间。书上的方法没太看懂,自己有一个思路,关键是基于这样的想法:如果所要求的数的位数是n,那么我们分为到n-1位最大数里面的1加上n位数里面的1,比如3位的345,那么就是0~99里面的1加上100~345里面的1。0~99也就是到最大2位的所有数字,我们可以递归求出其中含有“1”的个数,这个递归函数的思路是把10~99分为0~9(总位数少1,用递归去求)10~19(全部含1)以及20~29、30~39…90~99(最高位不含1,剩下的都是0~9,就可以递归去算个数,加上前面的0~9一共10个)。接下来要处理的就是100~345中“1”的个数,同样按照最高位划分。
public int total2(int n){
if(n <= 0)return 0;
int sum = 0;
//存储当前最高位数字
int high = n;
//存储当前位数
int count = 1;
//存储10^(count-1),比如n是3456,sup为1000
int sup = 1;
//初始化这3个值
while(high >= 10){
high /= 10;
count++;
sup *= 10;
}
//如果最高位数字等于1,可以直接计算结果
if(high == 1){
sum = n - sup + 1 + full(count - 1);
}else{//如果最高位数字大于1
//加上1开头的所有数字,这些数字满足要求,1000~1999
sum += sup;
//加上2~(high-1)开头的所有包含1的数字,以及位数少于n的所有数字的总和。2000~2999以及1~999
sum += (high - 1) * full(count - 1);
//加上high开头到n的所有包含1的数字,相当于去掉最高位。3000~3456
sum += total2(n - high * sup);
}
return sum;
}
//该函数返回1~9...9中包含1的数字的个数。也是分为最高位为1和最高位2-9
private int full(int count){
if(count <= 0)return 0;
if(count == 1)return 1;
int sum = 0;
int sup = 1;
for(int i=1; i<count; i++){
sup*=10;
}
sum += sup;
sum += 10*full(count-1);
return sum;
}
题目33:把数组排成最小的数
(输入一个正整数数组,把所有数字拼接起来排成一个数,打印出最小的那一个)
我的方法:正整数数组中的数连接成一个最小的数打印,肯定存在大数问题,要用字符数组或字符串去存储数。然后我就想到用回溯去求全排列,在尾端比较并存储。这样做的时间复杂度是n!相当于检查全排列的每一个数字。
public String buildNumber2(int[] a){
if(a == null || a.length == 0){
throw new IllegalArgumentException("输入数组不满足要求");
}
List<String> result = new ArrayList<>();
String line = "";
//寻找所有组合并比较大小
build2(result, line, 0, a.length, a);
return result.get(0);
}
private void build2(List<String> result, String line,int start, int border, int[] a){
if(start == border){
if(result.size() == 0){
result.add(new String(line));
}else{
if(isSmall(line, result.get(0))){
result.set(0, new String(line));
}
}
}
String now = new String(line);
for(int i=start; i <= border-1; i++){
Util.exch(a, start, i);
line += a[start];
build2(result, line, start+1, border, a);
line = now;
Util.exch(a, start, i);
}
}
//两个等长字符串,小于返回true,大于等于返回false
private boolean isSmall(String a, String b){
String one = a+b;
String two = b+a;
boolean isAlreadySmall = false;
for(int i=0; i<one.length(); i++){
if(one.charAt(i) < two.charAt(i)){
isAlreadySmall = true;
break;
}else if (one.charAt(i) == two.charAt(i)) {
continue;
}else{
break;
}
}
return isAlreadySmall;
}
书中方法:书上的方法没有完全看懂,在网上看到一个方法,也是利用了比较两个数字拼接后的两种组合的大小来确定两个int的位置顺序的思想,加上了快速排序思想。对于两个int数字x、y,可以组合成xy和yx,如果xy小于yx,那么对于这两个数,x就应该放在y前面,这是我们分割数组标准,按照快排的思路——如果我们对大数组按大小进行了分割,而且对分割后形成的两个小数组继续按大小进行分割,那么到最后这个数组一定是有序的。快排里的按大小就是数字的大小,我们这里也按一定的标准分割数组,这个标准就是前面所说的,效果就是大数组被分割成了应该放在分割元素前面的、分割元素和应该放在分割元素后面的,接着和快排一样我们继续分割。这里我对快排的理解更加深刻了,排序只是partition+递归的结果,如果我们更换partition中的分割条件,就能形成我们想要的效果,比如这里的拼接后的数字最小。
public String buildNumber(int[] a){
if(a == null || a.length == 0){
throw new IllegalArgumentException("输入数组不满足要求");
}
//不断以某个标准分割数组
build(a, 0, a.length-1);
String result = "";
for(int i=0; i<a.length; i++){
result += a[i];
}
return result;
}
private void build(int[] a, int start, int end){
if(start >= end)return;
//下标小于j的元素表述应该放在a[j]左边,下标大于j的元素表示应该放在a[j]右边
int j = partition(a, start, end);
build(a, start, j-1);
build(a, j+1, end);
}
private int partition(int[] a, int start, int end){
int target = a[start];
int left = start+1;
int right = end;
while(true){
while(left <= right &&
isSmall(String.valueOf(a[left]), String.valueOf(target))){
left++;
}
while(left <= right &&
!isSmall(String.valueOf(a[right]), String.valueOf(target))){
right--;
}
if(left == right + 1){
break;
}
Util.exch(a, left, right);
}
Util.exch(a, start, right);
return right;
}
//比较等长的两个字符串a,b,a小于b返回true,a大于等于b等于返回false
private boolean isSmall(String a, String b){
String one = a+b;
String two = b+a;
boolean isAlreadySmall = false;
for(int i=0; i<one.length(); i++){
if(one.charAt(i) < two.charAt(i)){
isAlreadySmall = true;
break;
}else if (one.charAt(i) == two.charAt(i)) {
continue;
}else{
break;
}
}
return isAlreadySmall;
}
题目34:丑数
书中方法一:对于一个数,先后不断除以因子2、3、5,如果最后能得到1,说明这个数只含有这些因子。这种方法的问题在于我们不仅对丑数进行了检查,而且对不是丑数的数进行了检查,而且“%”运算非常耗时。
public int find(int n){
if(n <= 0){
throw new IllegalArgumentException("输入参数不合法");
}
//记录已经找到的丑数的个数
int count = 0;
//记录即将要检查的数字
int result = 1;
while(count < n){
int copy = result;
//如果还包含有因子2
while(copy % 2 == 0){
copy /= 2;
}
//如果还包含有因子3
while(copy % 3 == 0){
copy /= 3;
}
//如果还包含有因子5
while(copy % 5 == 0){
copy /= 5;
}
if(copy == 1){
count ++;
}
result ++;
}
return --result;
}
书中方法二:我们不妨按顺序列出一些丑数并将他们因式分解,我们可以观察到后面的丑数都是由前面的丑数乘以2、3、5得到,但不一定是按顺序,例如1分别乘上2、3、5得到2、3、5,但是4也是丑数,它是有2乘上2得到。某个d位置的丑数应该是min(a位置丑数乘2,b位置丑数乘3,c位置丑数乘5),其中abc三个位置均小于d。那么我们用三个下标分别记录这三个位置。
public int find2(int n){
if(n <= 0){
throw new IllegalArgumentException("输入参数不合法");
}
if(n == 1){
return 1;
}
int[] result = new int[n];
result[0] = 1;
//将要填入丑数的位置
int index = 1;
//该位置的丑数还没有乘2
int index2 = 0;
//该位置的丑数还没有乘3
int index3 = 0;
//该位置的丑数还有没乘5
int index5 = 0;
while(index <= n-1){
//先找出三个丑数的最小值
int min = determineSmallest(result, index2, index3, index5);
//如果和最小值相等则移动指针表示已经使用,同时考虑避免重复的情况比如2*3和3*2
if(result[index2] * 2 <= min)index2++;
if(result[index3] * 3 <= min)index3++;
if(result[index5] * 5 <= min)index5++;
result[index++] = min;
}
return result[n-1];
}
private int determineSmallest(int[] a, int index2, int index3, int index5){
int min = a[index2] * 2 < a[index3] * 3 ? a[index2] * 2 : a[index3] * 3;
min = min < a[index5] * 5 ? min : a[index5] * 5;
return min;
}
题目35:第一个只出现一次的字符
书中方法一:想到最直观的方法是对于每个字符都往后扫描一遍,如果没有重复该字符就是只出现一次的字符,这种方法的时间复杂度是O(n^2)。这道题肯定要维护字符出现的次数,而出现的次数起码要扫描一遍字符串才能确定,所以时间复杂度肯定不小于O(n),否则信息获取不全。如果想在O(n)的时间内搞定,我们肯定需要额外的空间在存储信息并且这个空间的查找时间是常数级别的O(1),很自然想到散列表。Java中的char是16位unicode(0~65535),我们这里用LinkedHashMap来保存字符,出现过的字符次数加1,然后再遍历一遍寻找第一个只出现一次的字符,此时可以用迭代器(LinkedHashMap的迭代器默认为key的插入顺序)。
public char find2(String s){
LinkedHashMap<Character, Integer> map = new LinkedHashMap<>();
for(int i=0; i<s.length(); i++){
if(map.get(s.charAt(i)) == null){
map.put(s.charAt(i), 1);
}else{
map.put(s.charAt(i), map.get(s.charAt(i))+1 );
}
}
Iterator<Character> iterator = map.keySet().iterator();
while(iterator.hasNext()){
char c = iterator.next();
if(map.get(c) == 1){
return c;
}
}
throw new IllegalArgumentException("输入字符串不合法");
}
扩展1:输入两个字符串,从第一个字符串中删除在第二个字符串中出现过的所有字符。
首先把第二个字符串中的字符用HashMap存储起来,然后遍历第一个字符串,用StringBuilder添加。达到了利用额外空间进行快速查找的效果,时间复杂度是O(m)+O(n),额外空间是O(n)。
扩展2:删除字符串中所有重复出现的字符。
同样也是一边遍历一遍用HashMap(Character, Boolean) 存储,用StringBuilder添加,每当遍历到一个字符便进行查找,如果没有出现过就添加。
扩展3:判断两个单词是否为变位词(Anagram)。
由于单词只有26个字母,我们可以用一个大小为26的数组简单模拟哈希表。遍历单词A,将出现的字母字数统计出来,遍历单词B,出现的字母次数减1,然后遍历数组检查所有字母次数是否为0。
题目36:数组中的逆序对
书中方法:首先想到的肯定是O(n^2)的方法,对于每一个数字,遍历其后面的数字统计逆序对。要想提高速度,肯定要在找逆序对的方法上改进。我们利用这样一个思想:一个数组分为两个子数组且这两个子数组已经排序,那么很容易找出逆序对。因为子数组内不会有逆序对,只用统计两个子数组之间产生的逆序对。用两个指针(left,right)指向数组末尾,假设left元素大于right元素,那么对于left元素,逆序对数就是right-rightStart+1。由于我们要在有序的子数组上进行计算,所以想到了归并排序。归并排序利用了额外O(n)空间,排序时先复制到辅助数组,然后回填原数组排序。在回填的时候我们统计逆序对的个数。
public int find(int[] a){
if(a == null || a.length == 0 || a.length == 1){
return 0;
}
int[] sup = new int[a.length];
return find(a, 0, a.length-1, sup);
}
private int find(int[] a, int start, int end, int[] sup){
if(start >= end){
return 0;
}
int mid = start + (end - start)/2;
int result = 0;
//左边子数组排序并记录了逆序数
result += find(a, start, mid, sup);
//右边子数组排序并记录了逆序数
result += find(a, mid+1, end, sup);
//把左右子数组一起排序并记录逆序数
result += mergeAndCount(a, start, mid, end, sup);
return result;
}
private int mergeAndCount(int[] a, int start, int mid, int end, int[] sup){
//先复制到辅助数组用于回填
for(int i=start; i<=end; i++){
sup[i] = a[i];
}
//用于比较
int left = mid;
int right = end;
//用于回填
int index = end;
//记录逆序数
int result = 0;
//按序回填,其中左边子数组大于右边子数组的时候记录逆序数
while(left >= start && right >= mid+1){
if(sup[left] > sup[right]){
result += right-(mid+1)+1;
a[index--] = sup[left--];
}else{
a[index--] = sup[right--];
}
}
//当某一遍的子数组回填完时,剩下的子数组直接回填,不会产生逆序对。
while(left>=start){
a[index--] = sup[left--];
}
while(right >= mid+1){
a[index--] = sup[right--];
}
return result;
}
题目37:两个链表的第一个公共节点
书中方法:解决这道题的关键在于弄清楚如果两个链表存在公共节,它们会在公共节点之后重合。如果两个链表长度一样,我们可以直接从头开始比较;如果两个链表长度不一样,我们遍历两条链表时的起点就要以短的为标准,这样才能保证同时到达commen节点。
public ListNode find(ListNode first, ListNode second){
if(first == null || second == null){
return null;
}
int firstCount = 0;
int secondCount = 0;
ListNode firstMark = first;
ListNode secondMark = second;
//记录两条链表长度
while(first != null){
firstCount++;
first = first.next;
}
while(second != null){
secondCount++;
second = second.next;
}
//统一起点
int step = firstCount - secondCount;
first = firstMark;
second = secondMark;
if(step >= 0){
for(int i=1; i<=step; i++){
first = first.next;
}
}else{
for(int i=1; i<= -step; i++){
second = second.next;
}
}
//开始寻找公共节点
while(first != null){
if(first == second){
return first;
}
first = first.next;
second = second.next;
}
return null;
}
题目38:数字在排序数组中出现的次数
我的方法:看到排序数组首先想到二分查找,我们可以找到目标数字,然后顺序往两边查找,这样做的坏处是可能使时间复杂度为O(n)。我们要找的是边界,由于数组已经排序,我们并不用找到目标数字后再顺序查找每个数字,只需要找到边界即可。那么可以这么做:外层首先找到目标数字(二分查找),在找到目标数字的前提下,内层向两边找到边界(二分查找)。查找边界分为查找右边界和左边界,查找右边界的时候趋势是一直向右边寻找,查找左边界的时候趋势是一直向左边寻找。
public int getResult(int[] a, int target){
if(a == null || a.length == 0)return -1;
return findNumbers(a, 0, a.length-1, target);
}
//首先找到目标数字,然后计算并返回目标数字个数
private int findNumbers(int[] a, int start, int end, int target){
//如果数组中没有这个数字返回-1
if(start > end)return -1;
int mid = start+(end-start)/2;
//如果目标数字出现在中点
if(a[mid] == target){
//找到右边界
int left = findBorder(a, start, mid-1, target, false);
//找到左边界
int right = findBorder(a, mid+1, end, target, true);
//根据边界的返回值确定个数
if(left == -1 && right == -1){
return 1;
}else if(left == -1 && right != -1){
return right - mid + 1;
}else if(left != -1 && right == -1){
return mid - left + 1;
}else{
return right - left +1;
}
}else if(a[mid] < target){//在中点的右边继续寻找
return findNumbers(a, mid+1, end, target);
}else{//在中点的左边继续寻找
return findNumbers(a, start, mid-1, target);
}
}
//用于寻找边界(最后一个boolean用于控制寻找方向)
private int findBorder(int[] a, int start, int end, int target, boolean isFindRight){
//没有寻找到边界,返回-1
if(start > end){
return -1;
}
int mid = start+(end-start)/2;
//如果中点值等于目标而且是寻找右边界
if(a[mid] == target && isFindRight){
//继续在右半边寻找右边界
int result = findBorder(a, mid+1, end, target, true);
if(result == -1){
return mid;
}
return result;
}
//如果中点值等于目标而且是寻找左边界
if(a[mid] == target && !isFindRight){
//继续在左半边寻找左边界
int result = findBorder(a, start, mid-1, target, false);
if(result == -1){
return mid;
}
return result;
}
//如果中点值大于目标(只可能是在寻找右边界时发生),向左寻找右边界。如果中点值小于目标,向右寻找左边界
if(a[mid] > target){
return findBorder(a, start, mid-1, target, true);
}else return findBorder(a, mid+1, end, target, false);
}
书中方法:直接找到左边界和右边界。如果目标数字存在于数组中,左边界和右边界都不为-1,如果数字不存在数组中,返回-1.
public int getResult2(int[] a, int target){
if(a == null || a.length == 0)return -1;
//寻找左边界和右边界
int left = getLeftBorder(a, 0, a.length - 1, target);
int right = getRightBorder(a, 0, a.length-1, target);
//如果数字存在于数组中
if(left != -1 && right != -1){
return right - left + 1;
}
return -1;
}
//用二分法找到左边界
private int getLeftBorder(int[] a, int start, int end, int target){
if(start > end){
return -1;
}
int mid = start + (end-start)/2;
//如果中点值等于目标值
if(a[mid] == target){
//继续向左寻找
int result = getLeftBorder(a, start, mid-1, target);
if(result == -1){
return mid;
}
return result;
}
//如果中点值小于目标值,向右寻找左边界
if(a[mid] < target)return getLeftBorder(a, mid+1, end, target);
//如果中点值大于目标值,向左寻找左边界
return getLeftBorder(a, start, mid-1, target);
}
private int getRightBorder(int[] a, int start, int end, int target){
if(start > end){
return -1;
}
int mid = start + (end-start)/2;
if(a[mid] == target){
int result = getLeftBorder(a, mid+1, end, target);
if(result == -1){
return mid;
}
return result;
}
if(a[mid] < target)return getLeftBorder(a, mid+1, end, target);
return getLeftBorder(a, start, mid-1, target);
}
题目39:二叉树的深度
(1)输入一颗二叉树的根节点,求该树的深度。
public int find(TreeNode root){
if(root == null)return 0;
return Math.max(find(root.left), find(root.right)) + 1;
}
(2)输入一刻二叉树的根节点,判断该树是不是平衡二叉树。
我的方法:一开始考虑的是从顶向下判断每个节点是不是平衡的,也就是用前序遍历的方法,但是考虑到在确定树的深度的时候会产生重复的遍历(例如确定根节点的深度的时候我们要遍历其下每一个节点,由于没有记录节点的深度,我们在判断根节点的左子结点是否是平衡的时候又要遍历其下所有节点求深度,这就产生了重复的访问),我们所要做的第一步是把遍历改成后序遍历,目的是为了从底向上保存节点的深度,但此时还存在如何保存节点深度的问题。如果递归函数返回boolean值,那么就无法返回深度的信息,如果要返回深度的信息,我们就要额外创建引用容器作为一个参数传入(Java中是按值传递,如果传入的是基本变量,递归函数并不会改变该变量的值),可以想到我们要在递归函数中创建两个List left = new ArrayList<>();List right = new ArrayList<>();然后记录子节点的深度,最后返回该节点是否平衡和深度两个信息。我们换个角度想一想,我们要返回给上层的是子节点是否平衡和子节点深度两个信息,子节点平衡的信息是否能用boolean以外的值代替?如果我们在查找深度的函数中返回-1,表示该节点不平衡,返回其他的值(0或1)表示该节点平衡,就可以达到效果。
public boolean isBalanced(TreeNode root){
int result = findDepth(root);
if(result == -1)return false;
return true;
}
//计算节点的深度,如果该节点不平衡,返回-1
private int findDepth(TreeNode root){
if(root == null){
return 0;
}
int left = findDepth(root.left);
int right = findDepth(root.right);
//如果左子结点或右子节点有不平衡的,直接返回-1表示该节点不平衡
if(left == -1 || right == -1){
return -1;
}
//如果左子结点和右子节点的深度差大于1,返回-1表示该节点不平衡
if(Math.abs(left - right) > 1){
return -1;
}
//如果子节点的深度平衡,返回该节点的深度
return Math.max(left, right) + 1;
}
/*
用List的贴在下面
*/
public boolean isBalanced2(TreeNode root){
List<Integer> result = new ArrayList<>();
result.add(0);
return isBalanced2(root, result);
}
private boolean isBalanced2(TreeNode root, List<Integer> result){
if(root == null){
result.set(0, 0);
return true;
}
List<Integer> left = new ArrayList<>();
List<Integer> right = new ArrayList<>();
left.add(0);
right.add(0);
if(isBalanced2(root.left, left) && isBalanced2(root.right, right)){
int temp = 0;
if(Math.abs(left.get(0) - right.get(0)) <= 1){
result.set(0, Math.max(left.get(0), right.get(0))+1);
return true;
}
}
return false;
}
题目40:数组中只出现一次的数字
书中方法:一个整型数组里除了两个数字之外,其他的数组都出现了两次,寻找这两个只出现一次的数字,时间复杂度O(n),空间复杂度O(1)。一开始想只扫描1遍的话肯定要记录数字的个数,那么又要用到O(n)的额外空间,每个数字都出现2次这个条件也不知道怎么利用,完全没有头绪。书中给出的方法是利用了异或的性质:两个相同的数异或结果为0。我们把这个数组中所有元素异或,结果就是其中两个只出现一次的数的异或结果,其他的数字都两两抵消。具体的思路看书上的介绍。
public class FindTwoDistinctNumber {
public int[] find(int[] a){
if(a == null || a.length <= 1){
return null;
}
int result = 0;
for(int i=0; i<a.length; i++){
result ^= a[i];
}
int mark = findDifference(result);
if(mark == -1){
return null;
}
int first = 0;
int second = 0;
for(int i = 0; i<a.length; i++){
if((a[i] & mark) == mark){
first ^= a[i];
}else{
second ^= a[i];
}
}
return new int[]{first, second};
}
private int findDifference(int result){
int mark = 1;
for(int i=1; i<=32; i++){
if((mark & result) == mark){
return mark;
}
mark <<=1;
}
return -1;
}