算法 | 一周刷完《剑指Offer》 Day4:第38~49题

写在前面

  • 本系列包含《剑指Offer》66道算法题,预计一周刷完,这是第四篇。
    系列汇总:剑指Offer 66题 Java 刷题笔记汇总
  • 所有题目均可在牛客网在线编程平台进行调试。
    网址:https://www.nowcoder.com/ta/coding-interviews
  • 本系列包含题目,解题思路代码(Java)
    代码同步发布在GitHub:https://github.com/JohnnyJYWu/offer-Java

上一篇:算法 | 一周刷完《剑指Offer》 Day3:第27~37题
下一篇:算法 | 一周刷完《剑指Offer》 Day5:第50~60题


Day4:第38~49题

有几道题比较考逻辑和理解。到后面就简单了,好像更注重思维了。

  • T38. 二叉树的深度
  • T39. 平衡二叉树
  • T40. 数组中只出现一次的数字
  • T41. 和为S的连续正数序列
  • T42. 和为S的两个数字
  • T43. 左旋转字符串
  • T44. 翻转单词顺序列
  • T45. 二叉树的深度
  • T46. 孩子们的游戏(圆圈中最后剩下的数)
  • T47. 求1+2+3+…+n
  • T48. 不用加减乘除做加法
  • T49. 把字符串转换成整数

T38. 二叉树的深度

题目描述

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

解题思路

分别对左右子树递归计算深度,取深度更大的一个。

	public int TreeDepth(TreeNode root) {
		if(root == null) return 0;
		
		return 1//当前结点深度1
			+ Math.max(TreeDepth(root.left), TreeDepth(root.right));//分别对左右子树递归计算深度,取深度更大的一个
	}

T39. 平衡二叉树

题目描述

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

解题思路

平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

此题即将上一题计算左右子树深度【取更大】的思想转化为了计算左右子树深度【判断差值是否不超过1】。

	public boolean IsBalanced_Solution(TreeNode root) {
        if(root == null) return true;
        
        return getDepth(root) != -1;
    }
	
	private int getDepth(TreeNode root) {
		if(root == null) return 0;

		int left = getDepth(root.left);
        if(left == -1) return -1;
        
        int right = getDepth(root.right);
        if(right == -1) return -1;
        
        return Math.abs(left - right) > 1 ? -1 : 1 + Math.max(left, right);
	} 
    }

T40. 数组中只出现一次的数字

题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。

解题思路

重点:注意题意,两个数只出现一次,其他数都出现偶数次

方法一:HashSet。不包含则加入,包含则移除。最终出现偶数次的数一定都会被移除,仅留只出现了一次的数。

	//num1,num2分别为长度为1的数组。传出参数
	//将num1[0],num2[0]设置为返回结果
	public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {//HashSet
		Set<Integer> set = new HashSet<>();
		
		for(int num: array) {
			if(set.contains(num)) {
				set.remove(num);
			} else {
				set.add(num);
			}
		}
		
		Iterator<Integer> it = set.iterator();
		num1[0] = it.next();
		num2[0] = it.next();
	}

方法二:异或运算。此方法比较巧妙,需仔细理解。

首先理解二进制位运算的几个概念:
与运算( & ):运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
或运算( | ):0|0=0; 0|1=1; 1|0=1; 1|1=1;
异或运算( ^ ):运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;

异或运算有此特性:n^n=0;即任意数与本身异或得0;

所以对array数组所有数进行异或运算,得到的结果为两个仅出现一次的数异或运算的结果different(其他数由于出现偶数次,异或运算后都为0了),这个different是区分这两个数不同的标志

然后,或运算有此特性:n&-n取得n的二进制表示中最右边的1。(-n的表示百度补码)

对different进行与运算:different = different & (-different)。此时的different就成为了那个标志

最后再对array数组所有数进行一次异或运算,不同的是这一次要根据different将两个数区分开,填入结果。

	//num1,num2分别为长度为1的数组。传出参数
	//将num1[0],num2[0]设置为返回结果
	public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {//位运算
	    int different = 0;
	    for(int num: array) {//最终得到两个只出现一次的数相异的的结果
	    	different ^= num;
	    }
	    //得到最右边的1
	    different &= - different;
	    for(int num: array) {
	    	//由于different是那两个数像异的结果,那么这个取得的最右边的1,可将两个只出现一次的数区分开
	    	//然后再进行一遍异运算即可
	        if((num & different) == 0) {
	        	num1[0] ^= num;
	        } else {
	        	num2[0] ^= num;
	        }
	    }
	}

方法三:暴力解,不多赘述了。

	//num1,num2分别为长度为1的数组。传出参数
	//将num1[0],num2[0]设置为返回结果
	public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {//暴力解法
		ArrayList<Integer> list = new ArrayList<Integer>();
        Arrays.sort(array);
        
        int length = array.length;
        
        for(int i = 0; i < length; i ++){
            if(i == length - 1 && array[i] != array[i - 1]) {
                list.add(array[i]);
            }else if(i == 0 && array[i] != array[i + 1]) {
                list.add(array[i]);
            }else{
                if(i != 0 && i != length - 1 && array[i] != array[i - 1] && array[i] != array[i + 1]) {
                    list.add(array[i]);
                }
            }
        }
        
        num1[0] = list.get(0);
        num2[0] = list.get(1);
    }

T41. 和为S的连续正数序列

题目描述

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

输出描述:输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

解题思路

夹逼思想,定义正数序列的左边界small和右边界big,求small到big的和。和比所求小则big后移,比所求大则small后移。

	public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {//算法,夹逼
		ArrayList<ArrayList<Integer>> result = new ArrayList<>(); 
		if(sum <= 1) return result;
		
		int small = 1;
		int big = 2;
		
		while(small < (sum + 1) / 2) {//要求最少是两个数字,所以small最大为(s+1)/2
			int curSum = 0;//求small到big的和
			for(int i = small; i <= big; i ++) {
				curSum += i;
			}
			
			//夹逼思想,小则big后移,大则small后移
			if(curSum < sum) {
				big ++;
			} else if(curSum > sum) {
				small ++;
			} else {
				ArrayList<Integer> list = new ArrayList<>();
				for(int i = small; i <= big; i ++) {
					list.add(i);
				}
				result.add(list);
				small ++;
			}
		}
		
		return result;
    }

公式求解。

	public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {//公式
		ArrayList<ArrayList<Integer>> result = new ArrayList<>(); 
		if(sum < 3) return result;
		
		for(int i = 1; i <= sum / 2; i ++) {
			int value = 1 + 4 * i * i - 4 * i + 8 * sum;
			int valueSqrt = (int) Math.sqrt(value);
			if(value >= 25 && valueSqrt * valueSqrt == value) {
				ArrayList<Integer> list = new ArrayList<Integer>();
				for(int j = i; j <= (valueSqrt - 1) >> 1; j ++) {
					list.add(j);
				}
				result.add(list);
			}
		}
		return result;
    }

T42. 和为S的两个数字

题目描述

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

输出描述:对应每个测试案例,输出两个数,小的先输出。

解题思路

夹逼思想,且和相等时,差越大乘积越小。因此从两端向中间夹逼时第一个和为S的即为所求。

	public ArrayList<Integer> FindNumbersWithSum(int[] array, int sum) {
		//夹逼思想,且和相等时,差越大乘积越小
		ArrayList<Integer> result = new ArrayList<>();
		
		int small = 0;//数组下标,从前往后移
		int big = array.length - 1;//数组下标,从后往前移
        
		while(small <= big) {
			int curSum = array[small] + array[big];
			if(curSum < sum) {
				small ++;
			} else if(curSum > sum) {
				big --;
			} else {
				result.add(array[small]);
				result.add(array[big]);
				return result;
			}
		}
		
		return result;
    }

T43. 左旋转字符串

题目描述

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

解题思路

分三步走。(交换步骤顺序也可)

step1:先将左边3个字符串进行翻转:[abc]XYZdef --> [cba]XYZdef

step2:再将右边剩余字符串进行翻转:cba[XYZdef] --> cba[fedZYX]

step3:最后将整个字符串进行翻转: cbafedZYX --> XYZdefabc

	public String LeftRotateString(String str, int n) {
		if(str == null || str.length() == 0) return "";
		
		char[] chars = str.toCharArray();
		//step1:先将左边3个字符串进行翻转:[abc]XYZdef --> [cba]XYZdef
		reverse(chars, 0, n - 1);
		//step2:再将右边剩余字符串进行翻转:cba[XYZdef] --> cba[fedZYX]
		reverse(chars, n, chars.length - 1);
		//step3:最后将整个字符串进行翻转: cbafedZYX --> XYZdefabc
		reverse(chars, 0, chars.length - 1);
		
		return new String(chars);
    }
	
	private void reverse(char[] chars, int start, int end) {
		while(start < end) {
	        char tmp = chars[start]; 
	        chars[start] = chars[end]; 
	        chars[end] = tmp;
	        
	        start ++; 
	        end --;
	    }
	}

T44. 翻转单词顺序列

题目描述

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

解题思路

原理同上一题。先翻转每个单词的顺序,再翻转整个句子的顺序。(交换步骤顺序也可)

	public String ReverseSentence(String str) {//原理同T43
		if(str == null || str.length() == 0) return str;
		
		char[] chars = str.toCharArray();
		int length = chars.length;
		int startIndex = 0;//单词开始标记
		int endIndex = 0;//单词结束标记
		
		//翻转每个单词的字母顺序
		while(endIndex <= length) {
			if(endIndex == length || chars[endIndex] == ' ') {//遇到空格或到句末,翻转单词
				reversed(chars, startIndex, endIndex - 1);
				startIndex = endIndex + 1;
			}
			endIndex ++;		
		}
		
		//翻转整个句子的顺序
		reversed(chars, 0, length - 1);
        
        return new String(chars);
    }
	
	private void reversed(char[] chars, int start, int end) {
		while(start < end) {
	        char tmp = chars[start]; 
	        chars[start] = chars[end]; 
	        chars[end] = tmp;
	        
	        start ++; 
	        end --;
	    }
	}

T45. 二叉树的深度

题目描述

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即为需要用癞子替代的张数。题那么长都是废话,别想太多正常找就行。

	public boolean isContinuous(int[] numbers) {
		if(numbers == null || numbers.length < 5) return false;
		
		Arrays.sort(numbers);
		int sum0 = 0;//大小王数量
		for(int i = 0; i < numbers.length; i ++) {
			if(numbers[i] == 0) {
				sum0 ++;
			}
		}
		
		for(int i = sum0; i < numbers.length - 1; i ++) {
			if(numbers[i + 1] == numbers[i]) return false;//有相等的牌不可能为顺子
			
			int interval = numbers[i + 1] - numbers[i] - 1;//这两个数之间差了几张牌,需要用大小王代替
			if(interval > sum0) return false;//相差太大,大小王不够
			
			sum0 -= interval;
		}
		
		return true;
    }

T46. 孩子们的游戏(圆圈中最后剩下的数)

题目描述

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

解题思路

又是一堆废话,丢手绢游戏。。。
约瑟夫环,公式:
n = 1: f(n, m) = 0
n > 1: f(n, m) = [f(n - 1, m) + m] % n

	public int LastRemaining_Solution(int n, int m) {//约瑟夫环
		//公式:f(n, m) = 0                   (n = 1) 
		//f(n, m) = [f(n - 1, m) + m] % n     (n > 1)
		if(n == 0) return -1;
        if(n == 1) return 0;
	
        return (LastRemaining_Solution(n - 1, m) + m) % n;
	}

T47. 求1+2+3+…+n

题目描述

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

解题思路

递归相加。此题关键在于如何跳出递归,基本方向是采用逻辑与或的方式来计算,与的时候通过n>0来短路,这样在n=0的时候不需要计算递归的值,或的时候通过n==0来短路,在n=0的时候可以短路逻辑或运算。

	public int Sum_Solution(int n) {
        int sum = n;
        //boolean b = (n > 0) && (sum += Sum_Solution(n - 1)) > 0;
        boolean b = (n == 0) || (sum += Sum_Solution(n - 1)) > 0;
        
        return sum;
    }

T48. 不用加减乘除做加法

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

解题思路

位运算。a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。

	public int Add(int num1, int num2) {
		while(num2 != 0) {
	        int tmp = num1 ^ num2;
	        num2 = (num1 & num2) << 1;
	        num1 = tmp;
	    }
		
	    return num1;
    }

T49. 把字符串转换成整数

题目描述

将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

输入描述:
输入一个字符串,包括数字字母符号,可以为空
输出描述:
如果是合法的数值表达则返回该数字,否则返回0

解题思路

正常转换即可。

注意:正负号的存在,字符型代表的整形在转换过程中的计算。

	public int StrToInt(String str) {
	    if(str.length() == 0) return 0;
		
	    char[] chars = str.toCharArray();
	    boolean isNegative = chars[0] == '-';//判断是否有负号
	    
	    int result = 0;
	    
	    for(int i = 0; i < chars.length; i ++) {
	        if(i == 0 && (chars[i] == '+' || chars[i] == '-')) continue;//跳过正负号
	        
	        if(chars[i] < '0' || chars[i] > '9') return 0;
	        
	        result = result * 10 + (chars[i] - '0');
	    }
	    
	    return isNegative ? -result : result;//三元
    }

项目地址:https://github.com/JohnnyJYWu/offer-Java

上一篇:算法 | 一周刷完《剑指Offer》 Day3:第27~37题
下一篇:算法 | 一周刷完《剑指Offer》 Day5:第50~60题

希望这篇文章对你有帮助~

你可能感兴趣的:(剑指Offer)