21.连续子数组的最大和
题目描述
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
解题思路
入门级的动态规划题目,不难看出其递归关系:dp[i] = max{dp[i-1]+array[i],array[i]}。复杂度为O(n)。另外也试了一种复杂度为O(n^2)的算法,空间换时间,用一个二维数组记录所有的中间状态,其实使用一维数组就可解决,不想改了。
代码
/**
* 动态规划,代码有些冗余,不改了
* @param array 数组
* @return 连续数组的最大和
*/
public int FindGreatestSumOfSubArrayDp(int[] array) {
int row=array.length;
if(row==0) return 0;
int[] dp=new int[row];
dp[0]=array[0];
int max=array[0];
for(int i=1;i<row;++i){ //遍历数组元素
int tmp=(dp[i-1]+array[i]);
if(tmp>array[i]){
dp[i]=tmp;
}else
dp[i]=array[i];
if(dp[i]>max) max=dp[i]; //找到最大连续和
}
return max;
}
/**
* O(n^2)算法
* @param array
* @return
*/
public int FindGreatestSumOfSubArray(int[] array) {
int row=array.length;
int[][] sums=new int[row][row];
int max=Integer.MIN_VALUE;
for(int firstIntor=0;firstIntor<row;firstIntor++){
for(int i=0;i<=firstIntor;++i){
if(firstIntor==0) sums[i][firstIntor]=array[firstIntor];
else sums[i][firstIntor]=sums[i][firstIntor-1]+array[firstIntor];
if(sums[i][firstIntor]>max)
max=sums[i][firstIntor];
}
}
return max;
}
22.丑数
题目描述
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
解题思路
牛客上通俗易懂的解释:
首先从丑数的定义我们知道,一个丑数的因子只有2,3,5,那么丑数p = 2 ^ x * 3 ^ y * 5 ^ z,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2,3,5,就得到2,3,5三个丑数,在从这三个丑数出发乘以2,3,5就得到4,6,10,6,9,15,10,15,25九个丑数,我们发现这种方法会得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:
(1)丑数数组: 1
乘以2的队列:2
乘以3的队列:3
乘以5的队列:5
选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(2)丑数数组:1,2
乘以2的队列:4
乘以3的队列:3,6
乘以5的队列:5,10
选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(3)丑数数组:1,2,3
乘以2的队列:4,6
乘以3的队列:6,9
乘以5的队列:5,10,15
选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(4)丑数数组:1,2,3,4
乘以2的队列:6,8
乘以3的队列:6,9,12
乘以5的队列:5,10,15,20
选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;
(5)丑数数组:1,2,3,4,5
乘以2的队列:6,8,10,
乘以3的队列:6,9,12,15
乘以5的队列:10,15,20,25
选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列;
……………………
疑问:
1.为什么分三个队列?
丑数数组里的数一定是有序的,因为我们是从丑数数组里的数乘以2,3,5选出的最小数,一定比以前未乘以2,3,5大,同时对于三个队列内部,按先后顺序乘以2,3,5分别放入,所以同一个队列内部也是有序的;
2.为什么比较三个队列头部最小的数放入丑数数组?
因为三个队列是有序的,所以取出三个头中最小的,等同于找到了三个队列所有数中最小的。
实现思路:
我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步;“|”表示指针,arr表示丑数数组;
(1)1
|2
|3
|5
目前指针指向0,0,0,队列头arr[0] * 2 = 2, arr[0] * 3 = 3, arr[0] * 5 = 5
(2)1 2
2 |4
|3 6
|5 10
目前指针指向1,0,0,队列头arr[1] * 2 = 4, arr[0] * 3 = 3, arr[0] * 5 = 5
(3)1 2 3
2| 4 6
3 |6 9
|5 10 15
目前指针指向1,1,0,队列头arr[1] * 2 = 4, arr[1] * 3 = 6, arr[0] * 5 = 5
………………
代码
import java.util.*;
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index==0) return 0;
List<Integer> ret=new ArrayList<Integer>(); //存放前index个丑数
int p2=0,p3=0,p5=0; //分别是2倍、3倍和5倍丑数的遍历指针
ret.add(1);
while(ret.size()<index){
int min=Math.min(ret.get(p2)*2,Math.min(ret.get(p3)*3,ret.get(p5)*5)); //寻找下一个丑数
if(ret.get(p2)*2==min) p2++; //找到丑数后移动相应倍数下的指针(产生相等的情况同时移动)
if(ret.get(p3)*3==min) p3++;
if(ret.get(p5)*5==min) p5++;
ret.add(min);
}
return ret.get(index-1);
}
}
23.第一个只出现一次的字符
题目描述
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
解题思路
使用Map记录对应的键值对,很好解决。
代码
public int FirstNotRepeatingChar(String str) {
char[] data=str.toCharArray();
Map<Character,Integer> record=new HashMap<Character,Integer>();
for(int i=0;i<data.length;i++){
if(record.containsKey(data[i])){
if(record.get(data[i])<10000)
record.replace(data[i],record.get(data[i]),10001);
}else{
record.put(data[i],i);
}
} //检查有无重复字符,重复字符在map中的值记为10001,非重复字符在map的value为其在str中的位置
int firstPos=10001;
for(int tmp:record.values()){
if(tmp<firstPos)
firstPos=tmp;
} //找到第一个只出现一次字符的位置,即求value中值的最小值
return (firstPos<1001)?firstPos:-1;
}
24.两个链表的第一个公共结点
题目描述
输入两个链表,找出它们的第一个公共结点。
解题思路
初次遍历两链表找到链表长度,得到链表长度差n,较长链表指针先移动n步,然后两链表指针同时移动,每次移动时比较两链表指针指向结点是否相同,相同则为第一个公共结点。
另一种方法:因两链表在公共结点后的结点完全一样,所以可以从两链表尾部向前端遍历,直到找到第一个不一样的结点,此结点后的结点即为第一个公共结点。此算法开始时可先用两个栈存储两链表结点,以便于从尾部向前遍历。
代码
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
int p1Length=findLength(pHead1),p2Length=findLength(pHead2);
if(p1Length>p2Length) return doFind(pHead1,pHead2,p1Length-p2Length);
return doFind(pHead2,pHead1,p2Length-p1Length);
}
public int findLength(ListNode head){
ListNode p=head;
int length=0;
while(p!=null){
p=p.next;
++length;
}
return length;
} //寻找链表长度
public ListNode doFind(ListNode p1,ListNode p2,int n){
for(int i=0;i<n;++i)
p1=p1.next; //较长的链表先走n步
while(p1!=p2){
p1=p1.next;
p2=p2.next;
}
return p1;
}
}
25.二叉树的深度
题目描述
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
解题思路
递归计算左右子树深度,返回较大值。
代码
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public int TreeDepth(TreeNode root) {
int currentDeep=0; //初始深度为0
return findTreeDepth(root,currentDeep);
}
private int findTreeDepth(TreeNode node,int currentDeep){
if(node==null) return currentDeep; //当前结点为空,返回当前深度
//当前深度加1,递归计算左子树和右子树
return Math.max(findTreeDepth(node.left,currentDeep+1),
findTreeDepth(node.right,currentDeep+1));
}
}
26.数组中只出现一次的两个数字
题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。
解题思路
转牛客:首先我们考虑这个问题的一个简单版本:一个数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。
这个题目的突破口在哪里?题目为什么要强调有一个数字出现一次,其他的出现两次?我们想到了异或运算的性质:任何一个数字异或它自己都等于0 。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字,因为那些出现两次的数字全部在异或中抵消掉了。
有了上面简单问题的解决方案之后,我们回到原始的问题。如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其它数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。
我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其它数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0 ,也就是说在这个结果数字的二进制表示中至少就有一位为1 。我们在结果数字中找到第一个为1 的位的位置,记为第N 位。现在我们以第N 位是不是1 为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N 位都为1 ,而第二个子数组的每个数字的第N 位都为0 。
现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其它数字都出现了两次。因此到此为止,所有的问题我们都已经解决。
代码
public class Solution {
public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {
int xorSum=0;
for(int i=0;i<array.length;++i){
if(i==0) xorSum=array[0];
else xorSum^=array[i];
} //数组所有元素进行异或
int contour=0,tmp=-1; //左移记录数组分组的位置
while(tmp!=1){
tmp=xorSum&1;
xorSum=xorSum>>1;
if(contour==0) contour=1;
else contour=contour<<1;
} //寻找两个只出现一次数字的第一个不同位,以此位的0/1态作为划分子数组的依据
boolean recordNum1=false,recordNum2=false;
for(int i:array){
if((i&contour)!=0){
if(recordNum1) num1[0]^=i;
else {recordNum1=true;num1[0]=i;}
}else{
if(recordNum2) num2[0]^=i;
else {recordNum2=true;num2[0]=i;}
} //划分子数组,并在num1及num2中存放两个只出现一次的数字
}
}
}
27.和为S的两个数
题目描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
解题思路
由于数组有序,使用双指针,左端指针指向数组起始元素,右端指针指向数组尾部元素。比较两元素和tmpSum与sum的大小,若tmpSum>sum,则将右端指针左移一位后进行下一次判断;若tmpSum<sum,则将左端指针向右移动一位后进行下一次判断;若tmpSum==sum,则比较两元素乘积与当前记录乘积的大小关系,若小于,则记录此两个元素后左指针右移,右指针左移,进行下一次判断。
代码
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
int start=0,end=array.length-1; //双指针,一个初始化为数组开头,另一个初始化为数组结尾
int tmpSum=0,multiply=Integer.MAX_VALUE; //保存两个指针和的遍历结果及乘积
int small=Integer.MIN_VALUE,big=Integer.MAX_VALUE; //暂时记录和为sum的两个数
while(start<end){
tmpSum=array[start]+array[end];
if(tmpSum<sum) start++; //两数字和较小,移动左端指针
else if(tmpSum>sum) end--; //两数字和较大,移动右端指针
else{
int tmpMultiply=array[start]*array[end];
if(tmpMultiply<multiply){
small=array[start];
big=array[end];
multiply=tmpMultiply;
}
start++;end--;
} //相等时比较乘积,乘积小记录此两个数,并移动两指针
}
ArrayList<Integer> ret=new ArrayList<Integer>(); //存放结果
if(small!=Integer.MIN_VALUE) {ret.add(small);ret.add(big);}
return ret;
}
}
28.左旋转字符串
题目描述
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
解题思路
此题有多种解法,这里用的是字符串三次反转的方法。首先将左移次数对字符串长度取余,得到实际移动次数n。再将字符串前n位逆序,后(length-n)位逆序,接着整体逆序,就得到所需结果。
代码
public class Solution {
public String LeftRotateString(String str,int n) {
if(str==null||str.length()==0) return "";
n%=str.length();
StringBuffer str1=new StringBuffer(str.substring(0, n)); //反转前(n%str.length())个字符
str1.reverse();
StringBuffer str2=new StringBuffer(str.substring(n,str.length())); //反转(n%str.length()———str.length())个字符
str1.append(str2.reverse());
return str1.reverse().toString(); //整体反转
}
}
29.不用加减乘除做加法
题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
解题思路
对两个整数异或,相当于进行了无进位的加法;对两个整数的位与结果,相当于提取各位的进位;故将位与结果左移一位后与异或结果相加(进行下一次迭代的异或及位与),则得到整数相加结果。
代码
public class Solution {
public int Add(int num1,int num2) {
int xor=num1^num2,and=num1&num2;
while(and!=0) {
int tmp=xor;
xor^=(and<<=1);
and&=tmp;
}
return xor;
}
}
30.数组中重复的数字
题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解题思路
刚开始看这道题时,自然想到重新开辟一个length长度的数组(或Map对象更暴力),原数组中的元素值对应新数组的下标,新数组下标访问一次,该数组对应元素加一,遇到新数组元素值为2的,则找到重复数字(重复数字为新数组元素值为2的对应下标)。但还有一种不用额外开辟内存的方法,原理类似。
转牛客:不需要额外的数组或者hash table来保存,题目里写了数组里数字的范围保证在0 ~ n-1 之间,所以可以利用现有数组设置标志,当一个数字被访问过后,可以设置对应位上的数 + n,之后再遇到相同的数时,会发现对应位上的数已经大于等于n了,那么直接返回这个数即可。
代码
//int[] numbers:整数数组 length:numbers的长度 int[] duplication:duplication[0]存放重复的数字
//return true:有重复数字 return false:无重复数字
public boolean duplicate(int numbers[],int length,int [] duplication) {
for(int i=0;i<length;++i){
int seekPos=0; //数组中当前遍历元素应放入的位置
//根据当前遍历元素的数值计算其需放入的位置
if(numbers[i]>=length) seekPos=numbers[i]-length;
else seekPos=numbers[i];
//当需放入的位置上已放入一相等元素时,找到一对相等的数;若不相等,则该位置数值+length
if(numbers[seekPos]<length) numbers[seekPos]+=length;
else{
duplication[0]=seekPos;
return true;
}
}
return false;
}