目录
1、输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
3、输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
1、输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
方法一:非递归版
解题思路:
1
.核心是中序遍历的非递归算法。
2
.修改当前遍历节点与前一遍历节点的指针指向。
import java.util.Stack;
public TreeNode ConvertBSTToBiList(TreeNode root) {
if(root==null)
return null;
Stack<TreeNode> stack = new Stack<TreeNode>();
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
.根据左子树链表是否为空确定返回的节点。
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
点中的定位作了修改,新增一个全局变量记录左子树的最后一个节点。
// 记录子树链表的最后一个节点,终结点只可能为只含左子树的非叶节点与叶节点
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;
}
2、给一个数组,返回它的最大连续子序列的和
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
思路
看了几篇关于最大连续子序列的和的博客,发现都是上来给出状态方程:
max( dp[ i ] ) = getMax( max( dp[ i -1 ] ) + arr[ i ] ,arr[ i ] )
首先我们需要了解dp[i]到底是个啥,经过博主的不懈努力,终于发现dp[i]就是以数组下标为i的数做为结尾的最大子序列和,注意是以i为结尾,比如说现在有一个数组{6,-3,-2,7,-15,1,2,2},为了不搞,我们就下标以1开始,dp[3]就是以-2为结尾的,那么显然dp[3]的最大值就是1咯(6,-3,-2),dp[4]要以7结尾那么以7结尾的子序列最大和就是8(6,-3,-2,7)。
知道dp[i]是啥后,现在我们开始细细品一下上面这个递推式,求dp[i]的时候是不是有两种可能,要么就是像上面的dp[4]一样,dp[3]求出来是1了,再加上自己array[4]是最大的,那么还有一种可能就是说如果dp[3]我求出来是-100,那如果我也是dp[3]+array[4]的话是-93,这时候dp[3]反而是累赘,最大就是自己(因为前面定义了必须以i为结尾,也就说必须以7结尾)。
下面给出代码:
public int FindGreatestSumOfSubArray(int[] array) {
int max= array[0];
int res=array[0];
for(int i=1;i<array.length;i++){
max= Math.max(max+array[i],array[i]);
res=Math.max(max,res);//
}
return res;
}
3、输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
我们以三个字符abc为例来分析一下求字符串排列的过程。首先我们固定第一个字符a,求后面两个字符bc的排列。当两个字符bc的排列求好之后,我们把第一个字符a和后面的b交换,得到bac,接着我们固定第一个字符b,求后面两个字符ac的排列。现在是把c放到第一位置的时候了。记住前面我们已经把原先的第一个字符a和后面的b做了交换,为了保证这次c仍然是和原先处在第一位置的a交换,我们在拿c和第一个字符交换之前,先要把b和a交换回来。在交换b和a之后,再拿c和处在第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符b、a的排列。
public ArrayList<String> Permutation(String str) {
ArrayList<String> stringList = new ArrayList<String>();
char[] chars = str.toCharArray();
Permu(chars,0,stringList);
Collections.sort(stringList);
return stringList;
}
public void Permu(char[] chars,int n,ArrayList<String> strings){
if(chars==null){
return;
}
if(n==chars.length-1){
if(strings.contains(String.valueOf(chars))){
return;
}
strings.add(chars.toString());
}else {
for(int i=n;i<chars.length;i++){
char temp=chars[i];
chars[i]=chars[n];
chars[n]=temp;
Permu(chars,n+1,strings);
char temp1=chars[n];
chars[n]=chars[i];
chars[i]=temp1;
}
}
}
4、把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
* 解题思路:
* 先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就是制定排序规则。
* 排序规则如下:
* 若ab > ba 则 a > b,
* 若ab < ba 则 a < b,
* 若ab = ba 则 a = b;
* 解释说明:
* 比如 "3" < "31"但是 "331" > "313",所以要将二者拼接起来进行比较
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class Solution {
public String PrintMinNumber(int [] numbers) {
String[] arrayStr=new String[numbers.length];
for(int i=0;i<numbers.length;i++){
arrayStr[i]=String.valueOf(numbers[i]);
}
Arrays.sort(arrayStr, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
String c1=o1+o2;
String c2=o2+o1;
return c1.compareTo(c2);
}
});
StringBuffer sb =new StringBuffer();
for(int i=0;i<arrayStr.length;i++){
sb.append(arrayStr[i]);
}
return sb.toString();
}
}
5、丑数
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
该思路: 我们只用比较3个数:用于乘2的最小的数、用于乘3的最小的数,用于乘5的最小的
public int GetUglyNumber_Solution(int index) {
if (index <= 0) {
return 0;
}
int count = 0;
List<Integer> list = new ArrayList<Integer>();
list.add(1);
int i2 = 0, i3 = 0, i5 = 0;
while (list.size() <= index) {
int m2 = list.get(i2) * 2;
int m3 = list.get(i3) * 3;
int m5 = list.get(i5) * 5;
int min = Math.min(m2, Math.min(m3, m5));
list.add(min);
if (min == i2) i2++;
if (min == i3) i3++;
if (min == i5) i5++;
}
return list.get(list.size() - 1);
}
6、第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
说一下解题思路哈,其实主要还是hash,利用每个字母的ASCII码作hash来作为数组的index。首先用一个58长度的数组来存储每个字母出现的次数,为什么是58呢,主要是由于A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122,而每个字母的index=int(word)-65,比如g=103-65=38,而数组中具体记录的内容是该字母出现的次数,最终遍历一遍字符串,找出第一个数组内容为1的字母就可以了,时间复杂度为O(n)。
public int FirstNotRepeatingChar(String str) {
int[] word=new int[58];
for(int i=0;i<str.length();i++){
word[(int)str.charAt(i)-65]+=1;
}
for(int i=0;i<str.length();i++){
if(word[(int)str.charAt(i)-65]==1){
return i;
}
}
return -1;
}
7、数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:
题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
示例1
输入
复制
1,2,3,4,5,6,7,0
输出
复制
7
int count=0;
public int InversePairs(int [] array) {
if (array != null && array.length != 0) {
mergeUp2Down(array, 0, array.length - 1);
}
return count;
}
private void mergeUp2Down(int[] array, int low, int high) {
if(low>=high){
return;
}
int mid = (low + high) >> 1;
mergeUp2Down(array, low, mid);
mergeUp2Down(array, mid + 1, high);
merger(array,low,mid,high);
}
private void merger(int[] array, int low, int mid,int high) {
int i=low,j=mid+1,k=0;
int[] temp=new int[high-low+1];
while (i<=mid&&j<=high){
if(array[i]>array[j]){
temp[k++]=array[j++];
count+=mid-i+1;
count=count>1000000007?count%1000000007:count;
}else {
temp[k++]=array[i++];
}
}
while (i<=mid){
temp[k++]=array[i++];
}
while (j<=high){
temp[k++]=array[j++];
}
for(int m=0;m<temp.length;m++){
array[m+low]=temp[m];
}
}
8、两个链表的公共结点。
输入两个链表,找出它们的第一个公共结点。
假定 List1长度: a+n List2 长度:b+n, 且 a<b 那么 p1 会先到链表尾部, 这时p2 走到 a+n位置,将p1换成List2头部
接着p2 再走b+n-(n+a) =b-a 步到链表尾部,这时p1也走到List2的b-a位置,还差a步就到可能的第一个公共节点。
将p2 换成 List1头部,p2走a步也到可能的第一个公共节点。如果恰好p1==p2,那么p1就是第一个公共节点。 或者p1和p2一起走n步到达列表尾部,二者没有公共节点,退出循环。 同理a>=b. 时间复杂度O(n+a+b)
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while(p1!=p2){
if(p1!=null)p1=p1.next;
if(p2!=null)p2=p2.next;
if(p1 != p2){
if(p1==null)p1=pHead2;
if(p2==null)p2=pHead1;
}
}
return p1;
}
9、统计一个数字在排序数组中出现的次数。
因为data中都是整数,所以可以稍微变一下,不是搜索k的两个位置,而是搜索k-0.5和k+0.5
这两个数应该插入的位置,然后相减即可。
public int GetNumberOfK(int [] array , int k) {
return biSearch(array, k+0.5)-biSearch(array, k-0.5);
}
private int biSearch(int[] array, double v) {
int start=0,end=array.length-1;
while (start<=end){
int mid = (end - start)/2 + start;
if(array[mid]<v){
start=mid+1;
}else if(array[mid]>v){
end=mid-1;
}
}
return start;
}
10、二叉树的深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
求树的深度,可以从层次遍历出发考虑
层次遍历可以使用队列完成,也可以使用递归完成,所以有两种方法
递归版本:
思路: 从跟节点出发, 查询左子树的深度 , 获取右子树的深度,比较一下,取大的,再加一 。就是整个二叉树的深度
递归的三个条件
边界条件:当前节点下,是否还有子节点,没有返回,有继续递归
递归前进段:当前节点下,还有子节点
递归返回段:当前节点下,没有子节点
public int TreeDepth(TreeNode root) {
if(root==null){
return 0;
}
int left=TreeDepth(root.left);
int right=TreeDepth(root.right);
return Math.max(left,right)+1;
}
非递归版本:
public int TreeDepth(TreeNode root) {
if(root==null){
return 0;
}
// depth:当前节点所在的层数,count已经遍历了的节点数,
// nextCount下层的节点总数;当count==nextCount的时候,代表本层的节点已经遍历完毕。
int depth=0,count=0,nextCount=1;
ArrayList<TreeNode> list =new ArrayList<TreeNode>();
list.add(root);
while (list.size()>0){
TreeNode treeNode= list.remove(0);
count++;
if(treeNode.left!=null){
list.add(treeNode.left);
}
if(treeNode.right!=null){
list.add(treeNode.right);
}
if(count==nextCount){
count=0;
nextCount=list.size();
depth++;
}
}
return depth;
}
11、平衡二叉树
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
如果二叉树的每个节点的左子树和右子树的深度不大于1,它就是平衡二叉树。
先写一个求深度的函数,再对每一个节点判断,看该节点的左子树的深度和右子树的深度的差是否大于1
private boolean isBalance=true;
public boolean IsBalanced_Solution(TreeNode root) {
if(root!=null){
getTreeDepth(root);
}
return isBalance;
}
private int getTreeDepth(TreeNode root) {
if(root==null){
return 0;
}
int left=getTreeDepth(root.left);
int right=getTreeDepth(root.right);
if(Math.abs(left-right)>1){
isBalance=false;
}
return Math.max(left,right)+1;
}
12、数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
第一种解法,是自己想到的,使用HashMap可以把时间复杂度变为O(n);
public static void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
if(array.length<=1){
num1[0]=0;
num2[0]=0;
return;
}
HashMap<Integer,Integer> map= new HashMap<Integer,Integer>();
for(int i=0;i<array.length;i++){
if(map.containsKey(array[i])){
map.put(array[i],1);
}else {
map.put(array[i],0);
}
}
boolean isFirst=true;
for(Integer key:map.keySet()){
if(map.get(key)==0){
if(isFirst){
num1[0]=key;
isFirst=false;
}else {
num2[0]=key;
}
}
}
}
第二种解法是看别人的优秀写法。
考虑过程:
首先我们考虑这个问题的一个简单版本:一个数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。
这个题目的突破口在哪里?题目为什么要强调有一个数字出现一次,其他的出现两次?我们想到了异或运算的性质:任何一个数字异或它自己都等于0 。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字,因为那些出现两次的数字全部在异或中抵消掉了。
有了上面简单问题的解决方案之后,我们回到原始的问题。如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其它数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。
我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其它数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0 ,也就是说在这个结果数字的二进制表示中至少就有一位为1 。我们在结果数字中找到第一个为1 的位的位置,记为第N 位。现在我们以第N 位是不是1 为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N 位都为1 ,而第二个子数组的每个数字的第N 位都为0 。
现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其它数字都出现了两次。因此到此为止,所有的问题我们都已经解决。
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
if(array==null ||array.length<2)
return ;
int temp = 0;
for(int i=0;i<array.length;i++)
temp ^= array[i];
int indexOf1 = findFirstBitIs(temp);
for(int i=0;i<array.length;i++){
if(isBit(array[i], indexOf1))
num1[0]^=array[i];
else
num2[0]^=array[i];
}
}
private int findFirstBitIs(int num){
int indexBit = 0;
while(((num & 1)==0) && (indexBit)<8*4){
num = num >> 1;
++indexBit;
}
return indexBit;
}
private boolean isBit(int num,int indexBit){
num = num >> indexBit;
return (num & 1) == 1;
}
13、和为S的连续正数序列
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
输出描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
查看别人的写的优秀代码,总结了四种解法
解法一:
数学知识偏多一点,但也不是难到无法求解,稍微麻烦一点就是了。首先我们需要回答三个问题。
1、n = 2k + 1时,n项连续正数序列的和为S的条件: n & 1 && S / n == 0 解读 逻辑与的左边要求n为奇数,右边要求整个序列的平均数恰好为中间数。
2、n = 2k时,n项连续正数序列的和为S的条件: S % n * 2 == n 解读 S % n 的结果是中间两项左边的那项,乘2刚好是项数。举例,现有S = 39,6个连续正数序列和式能不能为S呢?套用公式,39 % 6 * 2 =6 == 6,我们也知道,这次的序列是 4、5、6、7、8、9,取余的结果为3对应着值为6的那一项,也就是中间项左边的那一项。
3、和为S,项数为n,如何写出这个序列? S / n - (n-1) / 2 解读 执行的除法是地板除法(floor),不管最终结果有无小数都直接舍去。仍使用上述例子,39 / 6 = 6,6恰好是中间项左边的那一项,6 - (6-1)/ 2 = 4,恰好是序列最左端。序列写出来就没问题。
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<ArrayList<Integer>>();
if (sum < 3) {
return listArrayList;
}
for (int n = (int) Math.sqrt(sum * 2); n >= 2; n--) {
if (((n & 1) == 1 && (sum % n) == 0) || ((sum % n) * 2 == n)) {
ArrayList<Integer> list = new ArrayList<>();
for (int j = 0, k = sum / n - (n - 1) / 2; j < n; j++, k++) {j用于计数,k用于遍历求值
list.add(k);
}
listArrayList.add(list);
}
}
return listArrayList;
}
解法二:
暴力求解,类似于TCP的滑动窗口协议。
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<>();
if (sum < 3) {
return listArrayList;
}
int low = 1, high = 2;
while (low < high) {
//终止条件,因为题干要求至少2个数
//连续且差为1的序列,求和公式可知
int currentSum = (int) ((low + high) * (high - low + 1)) / 2;//求和
if (currentSum < sum) {
high++;
}
if (currentSum > sum) {
low++;
}
if (currentSum == sum) {
ArrayList<Integer> list = new ArrayList<>();
for (int k = low; k <= high; k++) {
list.add(k);
}
low++;
listArrayList.add(list);
}
}
return listArrayList;
}
解法三:
同样是受到了TCP滑动窗口协议的启发。还有迭代的想法在里面,也可以是说状态转移方程。
用begin和end分别表示序列的左值和右值,首先将begin初始化为1,end初始化为2;
- 若[begin, end]之和 > S,从序列中去掉第一个值(增大begin);
- 若和 < S,增大end,和中加入新的end;
- 等于S,将[begin, end] 纳入到结果集中;
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<>();
if (sum < 3) {
return listArrayList;
}
int mid = (sum + 1) >> 1;
int begin = 1;
int end = 2;
int curSum = 3;
while (begin < mid) {
while ((curSum > sum)&&(begin<end)) {//这个循环的隐患在于循环结束时,beign == end而且和等于sum
curSum -= begin;
begin++;
}
if ((curSum == sum)&&(begin<end)) {
ArrayList<Integer> list = new ArrayList<>();
for (int k = begin; k <= end; k++) {
list.add(k);
}
listArrayList.add(list);
}
end++;
curSum += end;
}
return listArrayList;
}
解法四:(换元法)
根据所学知识,有:
(a + b)(b - a + 1) = 2 * sum;此为等差数列公式。
令i = b - a + 1(项数), j = a + b(首末项之和);现讨论取值范围。i >= 2(序列中至少有2项), j >= 3(序列之和至少为3);隐藏的关系是: j > i同时还有 i * j = 2 * sum,进行放缩之后就有 i * i < 2 * sum,即 i < 根号(2 * sum)。对i进行遍历,找出i,j∈正整数且j - i + 1为偶的取值。
ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<>();
if (sum < 3) {
return listArrayList;
}
int twoSum=2*sum;
int i,j;
for(i=(int)Math.sqrt(twoSum);i>=2;i--){
if(twoSum%i==0){//首末项之和为整数
j=twoSum/i;//求出首末项之和
int temp=j-i+1;//解方程得到2倍的左值等于j - i + 1;要求左边界为整数
if(temp%2==0){ //要求temp是偶数
int begin=temp>>1;
int end=j-begin;//j的实际意义是首末项之和
ArrayList<Integer> list = new ArrayList<>();
for (int k = begin; k <= end; k++) {
list.add(k);
}
listArrayList.add(list);
}
}
}
return listArrayList;
14、和为S的两个数字
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
输出描述:
对应每个测试案例,输出两个数,小的先输出。
假设:若b>a,且存在,
a + b = s;
(a - m ) + (b + m) = s
则:(a - m )(b + m)=ab - (b-a)m - m*m < ab;说明外层的乘积更小
也就是说依然是左右夹逼法!!!只需要2个指针
1.left开头,right指向结尾
2.如果和小于sum,说明太小了,left右移寻找更大的数
3.如果和大于sum,说明太大了,right左移寻找更小的数
4.和相等,把left和right的数返回
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> list=new ArrayList<>();
if(array.length<2){
return list;
}
int i=0,j=array.length-1;
while(i<j){
if((array[i]+array[j])==sum){
list.add(array[i]);
list.add(array[j]);
break;
}
if((array[i]+array[j])>sum){
j--;
}
if((array[i]+array[j])<sum){
i++;
}
}
return list;
}
15、反转单词顺序列
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
剑指offer的思想:两次翻转
public static String ReverseSentence(String str) {
if(str==null||str.trim().isEmpty()){
return str;
}
char[] c = str.toCharArray();
reverse(c,0,c.length-1);//翻转整个句子
int begin=0,end=0;
//翻转句子中的每个单词
while (begin<c.length){
if(c[begin]==' '){//若起始字符为空格,则begin和end都自加
begin++;
end++;
}else if(c[end]==' '){//遍历到终止字符为空格,就进行翻转
reverse(c,begin,--end);
begin=++end;
}else if(end==c.length-1){//若遍历结束,就进行翻转
reverse(c,begin,end);
begin=++end;
}
else{
end++;//没有遍历到空格或者遍历结束,则单独对end自减
}
}
return String.valueOf(c);
}
//完成翻转功能
private static void reverse(char[] c,int begin,int end) {
while (begin<end){
char temp= c[begin];
c[begin]=c[end];
c[end]=temp;
begin++;
end--;
}
}
16、扑克牌顺子
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
思路1:
1、排序 。
2、计算所有最大值和最小值间隔总数。
3、计算相邻的数字是否有重复。
public static boolean isContinuous(int [] numbers) {
if(numbers.length!=5){
return false;
}
int min = 14;
int max = -1;
int flag = 0;
Arrays.sort(numbers);//排序
for(int i=0;i<numbers.length;i++){
if(numbers[i]==0){
continue;
}
if((flag^numbers[i]) ==0)return false;//相邻的两个数异或运算,若为0则存在相等的
flag = numbers[i];
if(numbers[i]>max){
max=numbers[i];
}
if(numbers[i]<min){
min=numbers[i];
}
if (max - min >=5) {
return false;
}
}
return true;
}
看到别人还有一种写法,这个没有排序,有一点不太懂:
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++){
if(numbers[i]==0){
continue;
}
if(((flag >> numbers[i]) & 1) == 1) return false;//没看懂这里
flag = (1 << numbers[i]);//没看懂这里
if(numbers[i]>max){
max=numbers[i];
}
if(numbers[i]<min){
min=numbers[i];
}
if (max - min >=5) {
return false;
}
}
return true;
}
16、孩子们的游戏
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
/*
*这道题我用数组来模拟环,思路还是比较简单,但是各种下标要理清
*/
public int LastRemaining_Solution(int n, int m) {
if(n<1||m<1){
return -1;
}
int[] array =new int[n];
int i = -1,step = 0, count = n;
while (count>0) {//跳出循环时将最后一个元素也设置为了-1
i++;//指向上一个被删除对象的下一个元素。
if(i>=n){ //模拟环。
i=0;
}
if(array[i]==-1){//跳过被删除的对象。
continue;
}
step++;//记录已走过的。
if(step==m){ //找到待删除的对象。
array[i]=-1;
step=0;
count--;
}
}
return i;//返回跳出循环时的i,即最后一个被设置为-1的元素
}
17、求1+2+3+...+n
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
总结大牛们的方法,提供java的两种阶梯思路:
共同点:一,利用利用短路 && 来实现 if的功能;二,利用递归来实现循环while的功能
不同点:方法一:递归实现1+2+..+n;方法二:n(n+1)/2,递归实现n(n+1);方法三,利用Math实现n(n+1)
关于如何递归实现a*b,有大佬总结过,我搬下来:利用位运算来做,快速幂,快速模乘,
原理是把a拆成2的幂的和,a = 2^e0 + 2^e1 + 2^e2....
那么 a * b = (2^e0 + 2^e1 + 2^e2+...) * b
= b * 2^e0 + b * 2^e1 + b * 2^e2 + ...
= (b << e0) + (b << e1) + ....
接下来看代码:
方法一:递归实现1+2+..+n;
public int Sum_Solution(int n) {
int ans=n;
boolean flag=(ans>0)&&((ans+=Sum_Solution(n-1))>0);
return ans;
}
方法三,利用Math实现n(n+1)
public int Sum_Solution(int n) {
return (int)(Math.pow(n,2)+n)/2;
}
方法二:n(n+1)/2,递归实现n(n+1);
先参考使用while的例子,再转换
原理是把a拆成2的幂的和,a = 2^e0 + 2^e1 + 2^e2....
那么 a * b = (2^e0 + 2^e1 + 2^e2+...) * b
= b * 2^e0 + b * 2^e1 + b * 2^e2 + ...
= (b << e0) + (b << e1) + ....
public static int Sum_Solution2(int n) {
int res = 0;
int a = n;//若a=2=10
int b = n + 1;//b=3=11
while (a != 0) {
if ((a & 1) == 1)//a在第二位==1的时候才更新res=0+110=6
res += b;
a >>= 1;//a右移1位 1
b <<= 1;//b左移动1位 110
}
return res>>=1;//n(n+1)/2 }
接下来,用(a & 1) == 1和(a != 0)来代替判断语句
public int Sum(int n) {
int res = multi(n, n + 1);//n*(n-1)
return res>>=1;//n*(n-1)/2
}
private int multi(int a, int b) {
int res = 0;
//循环体内部, if ((a & 1) == 1), res += b;
boolean flag1 = ((a & 1) == 1) && (res += b) > 0;
a >>= 1;
b <<= 1;
// while (a != 0) {}循环条件
boolean flag2 = (a != 0) && (res += multi(a,b)) > 0 ;
return res;
}
18、不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
首先看十进制是如何做的: 5+7=12,三步走 第一步:相加各位的值,不算进位,得到2。 第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。 第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。 同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。 第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。 第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。 继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。
public int Add(int num1,int num2) {
while (num2!=0){
int temp=num1^num2;
num2=(num1&num2)<<1;
num1 =temp;
}
return num1;
}
19、把字符串转换成整数
将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。
输入描述:
输入一个字符串,包括数字字母符号,可以为空
输出描述:
如果是合法的数值表达则返回该数字,否则返回0
示例1
输入
+2147483647
1a33
输出
2147483647
0
public int StrToInt(String str) {
if(str==null||str.isEmpty()){
return 0;
}
char[] arr=str.toCharArray();
boolean isNec=false;
int sum=0;
if(arr[0]=='-'){
isNec=true;
}
for(int i=0;i<arr.length;i++){
if(i==0&&(arr[i]=='-'||arr[i]=='+')){
continue;
}
if(arr[i]>'9'||arr[i]<'0'){
return 0;
}
sum=sum*10+(arr[i]-'0');
}
return isNec?(0-sum):sum;
}
20、数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
思路:使用HashMap或者一个数组来存储数组中数出现的次数。
public static boolean duplicate(int numbers[],int length,int [] duplication) {
if(length<2||numbers.length<2){
return false;
}
boolean isReturn=false;
HashMap<Integer,Integer> map=new HashMap<>();
for(int i=0;i<numbers.length;i++){
if(map.containsKey(numbers[i])){
int value =map.get(numbers[i]);
map.put(numbers[i],++value);
}else {
map.put(numbers[i],0);
}
}
for (int key : map.keySet()) {
if(map.get(key)!=0){
duplication[0]=key;
isReturn=true;
break;
}
}
return isReturn;
}
另一种:
/**
* 数组中重复的数字
*在一个长度为n的数组里的所有数字都在0到n-1的范围内。
* 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
* 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
* 思路:
* 数组中的数字都在0到n-1的数字范围内。如果数组中没有重复出现的数字,那么当数组排序后数字i就出现在数组中下标为i的元素处。那么数组中如果存在重复数字的话,有些位置的对应的数字就没有出现,而有些位置可能存在多个数字。数组用numbers表示
那么我们重排这个数组。从第0个元素开始。
1、比较numbers[i]和i的值,如果i与numbers[i]相等,也就是对数组排序后,numbers[i]就应该在对应的数组的第i个位置处,那么继续判断下一个位置。
2、如果i和numbers[i]的值不相等,那么判断以numbers[i]为下标的数组元素是什么。
2.1、如果numbers[numbers[i]]等于numbers[i]的话,那么就是说有两个相同的值了,重复了。找到了重复的数字
2.2、如果numbers[numbers[i]]不等于numbers[i]的话,那么就将numbers[numbers[i]]和numbers[i]互换。继续进行1的判断。
3、循环退出的条件是直至数组最后一个元素,仍没有找到重复的数字,数组中不存在重复的数字。
*/
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(length<=0||numbers==null){
return false;
}
//判断数组数据是否合法
for(int i=0;i<length;i++){
if(numbers[i]<0||numbers[i]>length-1){
return false;
}
}
for(int i=0;i<length;i++){
while(numbers[i]!=i){
if(numbers[i]==numbers[numbers[i]]){
duplication[0] = numbers[i];
return true;
}else{
//交换numbers[i]和numbers[numbers[i]]
int temp = numbers[i];
numbers[i] = numbers[temp];
numbers[temp] = temp;
}
}
}
return false;
}
21、构建乘积数组
给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。
剑指的思路:
B[i]的值可以看作下图的矩阵中每行的乘积。
下三角用连乘可以很容求得,上三角,从下向上也是连乘。
因此我们的思路就很清晰了,先算下三角中的连乘,即我们先算出B[i]中的一部分,然后倒过来按上三角中的分布规律,把另一部分也乘进去。
public int[] multiply(int[] A) {
int[] B = new int[A.length];
B[0]=1;
for(int i=1;i<A.length;i++){
B[i]=B[i-1]*A[i-1];
}
int temp=1;
for(int i=A.length-2;i>=0;i--){
temp=temp*A[i+1];
B[i]=B[i]*temp;
}
return B;
} B;
22、正则表达式匹配
请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配
public boolean match(char[] str, char[] pattern)
{
if (str == null || pattern == null) {
return false;
}
int strIndex = 0;
int patternIndex = 0;
return matchCore(str, strIndex, pattern, patternIndex);
}
private boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
//str到尾,pattern到尾,匹配成功
if (strIndex == str.length && patternIndex == pattern.length) {
return true;
}
//str未到尾,pattern到尾,匹配失败
if (strIndex != str.length && patternIndex == pattern.length) {
return false;
}
//str到尾,pattern未到尾(不一定匹配失败,因为a*可以匹配0个字符)
if (strIndex == str.length && patternIndex != pattern.length) {
//只有pattern剩下的部分类似a*b*c*的形式,才匹配成功
if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
return matchCore(str, strIndex, pattern, patternIndex + 2);
}
return false;
}
//str未到尾,pattern未到尾
if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
if (pattern[patternIndex] == str[strIndex] || (pattern[patternIndex] == '.' && strIndex != str.length)) {
return matchCore(str, strIndex, pattern, patternIndex + 2)//*匹配0个,跳过
|| matchCore(str, strIndex + 1, pattern, patternIndex + 2)//*匹配1个,跳过
|| matchCore(str, strIndex + 1, pattern, patternIndex);//*匹配1个,再匹配str中的下一个
} else {
//直接跳过*(*匹配到0个)
return matchCore(str, strIndex, pattern, patternIndex + 2);
}
}
if (pattern[patternIndex] == str[strIndex] || (pattern[patternIndex] == '.' && strIndex != str.length)) {
return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
}
return false;
}
23、表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。
1、看到一个大神的暴力解法,十分浅显易懂。
public boolean isNumeric(char[] str) {
try {
double d= Double.parseDouble(new String(str));
}catch (Exception e){
return false;
}
return true;
}
2、一般解题思路:
12e
说明e的后面必须有数字,不能有两个e+-5
说明符号位要么出现一次在首位,要么出现一次在e的后一位,其他地方都不能有12e4.3
说明e的后面不能有小数,1.2.3
说明不能有两个小数点1a3.14
说明不能有其他的非法字符,比如这里的a
public boolean isNumeric(char[] str) {
//signal表示符号,decimal表示小树点,hasE表示含有符号e
boolean signal = false,decimal = false,hasE = false;
for(int i=0;i<str.length;i++){
if(str[i] == 'E' || str[i] == 'e'){
//e后面必须有数字,所以是最后一位肯定不通过
if(i==str.length-1){
return false;
}
//不能有两个e
if(hasE){
return false;
}
hasE = true;
}else if(str[i] == '+' || str[i] == '-'){
//不是第一次出现,那么后面能出现符合的地方只有紧贴着e的后面一位,不是则不通过
if(signal && str[i-1] != 'E' && str[i-1] != 'e'){
return false;
}
//第一次出现,如果不是出现在第一位,那么还是判断一下是不是出现在e的后面一位
if(!signal && i>0 && str[i-1] != 'E' && str[i-1] != 'e'){
return false;
}
signal = true;
}else if(str[i] == '.'){
//如果存在e并且e后面为小数则不通过
if(hasE){
for(;i>=0;i--){
if(str[i] == 'e' || str[i] == 'E'){
return false;
}
}
}
//不能有两个小数点
if(decimal){
return false;
}
decimal = true;
}else if(str[i] < '0' || str[i] > '9'){
//不是e也不是+-符号也不是小数点,那么只能是数字,不是数字就是非法的字符
return false;
}
}
return true;
}
3、正则表达式解法
/*
以下对正则进行解释:
[\\+\\-]? -> 正或负符号出现与否
\\d* -> 整数部分是否出现,如-.34 或 +3.34均符合
(\\.\\d+)? -> 如果出现小数点,那么小数点后面必须有数字;
否则一起不出现
([eE][\\+\\-]?\\d+)? -> 如果存在指数部分,那么e或E肯定出现,+或-可以不出现,
紧接着必须跟着整数;或者整个部分都不出现
*/
public boolean isNumeric(char[] str) {
String string = String.valueOf(str);
return string.matches("[\\+\\-]?\\d*(\\.\\d+)?([eE][\\+\\-]?\\d+)?");
}
24、字符流中第一个不重复的字符
常规的解法是用一个map来存储,这样空间复杂度为O(n),然后每次都遍历map获取第一个不重复的字符,时间复杂度也为O(n)。下面显示代码:
Map<Character,Integer> map =new LinkedHashMap<>();
public void Insert(char ch)
{
if(map.containsKey(ch)){
int m= map.get(ch);
map.put(ch,++m);
}else {
map.put(ch,0);
}
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
for(char ch:map.keySet()){
int k = map.get(ch);
if(k==0){
return ch;
}
}
return '#';
}
25、链表中环的入口结点
//先说个定理:两个指针一个fast、一个slow同时从一个链表的头部出发
//fast一次走2步,slow一次走一步,如果该链表有环,两个指针必然在环内相遇
//此时只需要把其中的一个指针重新指向链表头部,另一个不变(还在环内),
//这次两个指针一次走一步,相遇的地方就是入口节点。
//这个定理可以自己去网上看看证明。
public ListNode deleteDuplication(ListNode pHead) {
ListNode fast = pHead;
ListNode slow = pHead;
while (fast!=null&&fast.next!=null){
fast = fast.next.next;
slow= slow.next;
if(fast.val==slow.val){
break;
}
}
if(fast==null||fast.next==null){
return null;
}
fast=pHead;
while (fast!=slow){
fast = fast.next;
slow= slow.next;
}
return fast;
}