题目描述:
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解题思路:
很明显,由于该二维数组上到下递增,左到右递增的特殊性,遍历整个矩阵进行查找不是该题目的意图所在。总结规律我们可以发现:应该从矩阵的右上角或者左下角开始查找。
以右上角为例,首先选取右上角的数字,如果该数字等于要查找的数字,则查找过程结束;如果该数字大于要查找的数字,则说明该列其他元素都大于要查找的数字,便可以删掉该列;如果该数字小于要查找的数字,则说明该行其他元素也都小于要查找的数字,便可以删掉该行。
这样,每一次比较都可以剔除一行或者一列,进而缩小查找范围,时间复杂度为O(n)。
举例:
比如在下面的二维数组中查找数字7,查找过程如下:
编程实现(Java):
(复习时直接看最下面的函数)
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
//如果数组为空,则直接返回false
if(matrix==null || matrix.length == 0 ) {
return false;
}
//从右上角开始找
int i=0,j=0;
for(i =0,j=matrix[0].length-1; i<matrix.length && j>=0;) {
if(matrix[i][j] == target) {
return true; //如果找到直接返回true
}
else if(matrix[i][j]>target) { //如果该值大于查找值,跳过该列
j--;
}
else { //如果该值小于查找值,跳过该行
i++;
}
}
return false; //最后记得返回false,不然会报错
}
}
题目描述:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
解题思路:
本题的直观解法很简单,直接对数组进行一次遍历就可以找到最小值,复杂度为O(n),但是显然这不是本题的意图所在,因为没有利用到任何旋转数组的特性。
进一步分析,如果整个数组是有序的,那我们一定会想到用折半查找来实现。对于旋转数组,我们发现,它实际上可以划分为两个排序的子数组,而且前面数组的元素都不小于后面数组的元素,并且最小值正好就是这两个数组的分界线,由此,我们可以得出以下解决方法。
首先用两个指针low和high分别指向数组的第一个元素和最后一个元素,然后可以找到中间元素mid。对于这个中间元素,有以下两种情况:(1)该元素大于等于low指向的元素,此时最小的元素说明在mid的后面,可以把low=mid;(2)中间元素小于等于high指向的元素,那么最小元素在mid之前,可以high=mid。特别注意:这里不要+1或者-1,因为只有这样才能保证low始终在第一个数组,high始终在第二个数组。依次循环,当最后low和high相差1时,low指向第一个数组的最后一个,high指向第二个数组的第一个(即为我们要找的最小值)。
很明显,以上查找的时间复杂度为O(logN)。
除此之外,本题还有两个特殊情况:
将数组前0个元素移动到后面(相当于没有旋转,数组整体有序)。明显我们上面的分析没有包含这种情况,需要特殊处理,方法也很简单,将第一个元素和最后一个元素相比,若第一个元素小于最后一个元素,则说明最小值就是的第一个元素,可以直接返回。
首尾指针指向的数字和中间元素三者都相等时,无法判断中间元素位于哪个子数组,无法缩小问题规模。此时,只能退而求其次,进行顺序查找。
编程实现(Java):
import java.util.Arrays;
public class Practise_02 {
public static void main(String[] args) {
int[] array = new int[]{1,1,0,0,1};
System.out.println("数组为:"+Arrays.toString(array));
System.out.println("数组最小值为:"+findMinNumber(array));
}
/*
三种情况:
(1)数组没有发生旋转,即把前面0个元素搬到末尾,也就是排序数组本身,第一个就是最小值
(2)一般情况二分查找,当high-low=1时,high就是最小值
(3)如果首尾元素和中间元素都相等时,只能顺序查找
*/
private static int findMinNumber(int[] array) {
int len = array.length;
int low = 0,high = len-1;
//如果长度为0,返回0
if(len==0) {
return -1;
}
//如果长度为1,直接返回该值
if(len==1) {
return array[0];
}
//第一种情况,数组没有发生旋转,即数组本身,最小值就是第一个元素
if(array[low]<array[high]) {
return array[0];
}
//第二种情况,因为是排序数列,因此用二分查找,时间复杂度为O(logn)
while(low<high) {
int mid = (low + high) / 2 ;
//当首尾和中间元素相等时,因为没法比较谁大水小,因此用不了折半查找,只能用顺序查找
if(array[low] == array[mid] || array[mid] == array[high]) {
return orderFind(array);
}
if(array[low]<array[mid]) {
low = mid;
}
else if(array[mid]<array[high]) {
high = mid;
}
if(high-low == 1) {
return array[high];
}
}
return -1;
}
//顺序查找的方法
private static int orderFind(int[] array) {
int min = array[0];
for(int i=0;i<array.length;i++) {
if(array[i]<min) {
min = array[min];
}
}
return min;
}
}
题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解题思路:
首先,如果不考虑奇数和奇数,偶数和偶数的相对位置,那么我们有一种双指针解法来求解,类似于快排,维护两个指针,第一个指针指向数组的第一个数字,第二个指针指向数组的最后一个数字。第一个指针向后移,第二个指针向前移,如果第一个指针指向偶数,第二个指针指向的是奇数,则交换着两个数字,接着继续移动直到两指针相遇。
上面的方法看似不错,但是对本题不适用,因为本题有相对位置不变的要求,直接交换会导致相对位置改变。因此,我们采用下面的思路来解决本题。
本题解法:对数组进行遍历,设置两个指针even和odd,even指向当前第一个偶数,odd从这个偶数之后开始查找,找到第一个奇数,此时为了相对位置不变,不能直接交换even和odd,而是将从even到odd-1的元素都依次向后移一个位置,将odd指向的那个奇数放到even的位置。然后再找下一个偶数,重复这一过程,最终就可以将奇数都放到偶数的前面,并且保证了相对位置的不变。
编程实现(Java):
public class Practise_03 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3,4,5};
//输出的数组结果
for(int i=0;i<array.length;i++) {
System.out.print(array[i]+" ");
}
reOrderArray(array);
//输出调整后的数组结果
for(int i=0;i<array.length;i++) {
System.out.print(array[i]+" ");
}
}
private static void reOrderArray(int[] array) {
int even = 0, odd = 0;
while (even < array.length && odd <array.length) {
//找到第一个偶数
while(even < array.length && array[even]%2!=0) {
even++;
}
odd = even + 1;
//找到第一个偶数后的第一个奇数
while(odd < array.length && array[odd]%2==0) {
odd++;
}
//注意判断,防止溢出
if(odd>=array.length) {
break;
}
//把偶数与奇数之间的所有偶数往后移动,奇数与该偶数位置的元素对调
int temp = array[odd]; //先把偶数后第一个奇数保存下来,因为接下来的移动过程中会替换掉该奇数
for (int k = odd - 1; k >= even; k--) {
array[k + 1] = array[k];
}
array[even] = temp; //把奇数填到第一个偶数往后移动,所腾出来的位置
even++;
//问题:为什么循环最后只要even++;而不用odd++?
//答案:因为主要是找even,也就是第一个偶数,odd是该偶数后第一个奇数。
}
}
}
题目描述:
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
解题思路:
我们把打印分成了四步:
第一步:从左到右打印一行
第二步:从上到下打印一列
第三步:从右到左打印一行
第四步:从下到上打印一列
public int[] spiralOrder(int[][] matrix) {
if(matrix.length == 0) return new int[0];
int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
int[] res = new int[(r + 1) * (b + 1)];
while(true) {
for(int i = l; i <= r; i++) res[x++] = matrix[t][i]; // left to right.
if(++t > b) break;
for(int i = t; i <= b; i++) res[x++] = matrix[i][r]; // top to bottom.
if(l > --r) break;
for(int i = r; i >= l; i--) res[x++] = matrix[b][i]; // right to left.
if(t > --b) break;
for(int i = b; i >= t; i--) res[x++] = matrix[i][l]; // bottom to top.
if(++l > r) break;
}
return res;
}
问题:为什么判断条件是columns>start2 && rows>start2?
答案:代入输入的例子,行数是3,start最大是1,得以不断循环。
题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如:输入如下所示的一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解题思路:
本题有以下三种方法可解:
方法一:首先对数组进行排序,在一个有序数组中,次数超过一半的必定是中位数,那么可以直接取出中位数,然后遍历数组,看中位数是否出现次数超过一半,这取决于排序的时间复杂度,最快为O(nlogn)。
方法二:遍历数组,用 HashMap 保存每个数出现的次数,这样可以从map中直接判断是否有超过一半的数字,这种算法的时间复杂度为O(n),但是这个性能提升是用O(n)的空间复杂度换来的。
方法三(最优解法):根据数组特点得到时间复杂度为O(n)的算法。根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和还要多。 因此,我们可以在遍历数组的时候设置两个值:一个是数组中的数result,另一个是出现次数times。当遍历到下一个数字的时候,如果与result相同,则次数加1,不同则次数减一,当次数变为0的时候说明该数字不可能为多数元素,将result设置为下一个数字,次数设为1。这样,当遍历结束后,最后一次设置的result的值可能就是符合要求的值(如果有数字出现次数超过一半,则必为该元素,否则不存在),因此,判断该元素出现次数是否超过一半即可验证应该返回该元素还是返回0。这种思路是对数组进行了两次遍历,复杂度为O(n)。
//思路2:用hashmap保存每个数出现的次数
public int MoreThanHalfNum_Solution(int [] array) {
if(array==null)
return 0;
Map<Integer,Integer> res=new HashMap<>();
int len = array.length;
for(int i=0;i<array.length;i++){
res.put(array[i],res.getOrDefault(array[i],0)+1);
if(res.get(array[i])>len/2)
return array[i];
}
return 0;
}
//思路3:根据数组特点得到时间复杂度为O(n)的算法
public int MoreThanHalfNum_Solution(int [] array) {
if(array==null||array.length==0)
return 0;
int len = array.length;
int result=array[0];
int times=1;
for(int i=1;i<len;i++){
if(times==0){
result=array[i];
times=1;
continue; //continue的意思是结束本次循环,也就是本次循环的下面代码不执行了
}
if(array[i]==result)
times++;
else
times--;
}
//检查是否符合
times=0;
for(int i=0;i<len;i++){
if(array[i]==result)
times++;
if(times>len/2)
return result;
}
return 0;
}
题目描述:
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)。
解题思路:
本题可以看做是一个多阶段决策找最优解的问题,因此可以用典型的动态规划思想来求解。用 res[ i ] 表示以第 i 个元素结尾的子数组的最大和,那么有以下递推公式:res[ i ]=max(res[ i-1]+data[ i ],data[ i ]).
这个公式的含义是:当以第i-1个数字结尾的子数组中所有数字的和小于0时,把这个负数与第i个数累加,则得到的和比第i个数字本身还要小,所以这种情况下res[ i ]就是第i个数字本身。反之,如果以第i-1个数字结尾的子数组中所有数字的和大于0,则与第i个数字累加就得到以第i个数字结尾的子数组中所有数字的和。
public int findMaxSumOfArray(int[] array) {
/*
动态规划,用res[i]表示以第i个元素结尾的最大和
res[i]中最大者即为最大连续子序列的和
res[i]=max(res[i-1] + data[i] , data[i])
*/
if(array==null||array.length==0)
return Integer.MIN_VALUE;
int endAsI=array[0]; //endASI就是res[i-1]
int result=endAsI; //result存取最大的结果,最后返回的就是这个
for(int i=1;i<array.length;i++){
endAsI=endAsI+array[i]>array[i] ? endAsI+array[i] : array[i];
if(endAsI>result)
result=endAsI;
}
return result;
}
题目描述:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路:
本题最直观的解法就是求出数组中所有数字的全排列,然后比较所有的排列,最后找到最小的排列,但是时间复杂度为O(n!),所以不是一个好的解法。
换一种思路可以发现,本题实际上希望我们找到一个排序规则,数组根据这个排序规则进行重排之后可以连成一个最小的数字。要确定这样的排序规则,也就是对于两个数字m和n,通过一个规则确定哪个应排在前面。
根据题目要求,我们可以发现,两个数字m和n能拼接成mn和nm,如果mn 若mn>nm,则m大于n 编程实现(Java): 题目描述: 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。 输入描述: 题目保证输入的数组中没有的相同的数字数据范围: 对于%50的数据,size<=10^4 本题一个最容易想到的解法是暴力解法,顺序扫描整个数组,每扫描到一个数字时,逐个比较该数字与后面的数字的大小关系,统计逆序对的个数,假设数组中有n个数字,则每个数字都要和O(n)个数字做比较,因此,这个暴力解法的时间复杂度为O(n^2)。 一般情况下,最容易想到的往往不是最优解法。在这里,我们采用分治的思想,类比归并排序算法来分析此题。 首先将数组分隔成子数组,统计出子数组内部逆序对数目,然后再统计相邻子数组之间的逆序对数目,统计过程中还需要对数组进行排序,这实际上就是归并排序的过程。主要考虑的是合并两个有序序列时,计算逆序对数。对于两个有序升序序列,设置两个下标分别指向开始位置,每次比较两个指针对应的值,如果第一个序列当前值大于第二个序列当前值,则有第一个序列“当前长度”个逆序对。 这看起来好像比较拗口,但是从代码中可以直观看出。 题目描述: 统计一个数字在排序数组中出现的次数。例如,输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于数字3在该数组中出现了4次,所以函数返回4。 解题思路: 既然输入的数组是有序的,所以我们就能很自然的想到用二分查找算法。以题目中给的数组为例,一个比较自然的想法是用二分查找先找到一个3,由于要计算的是输出的次数,所以需要在找到的这个3的左右两边分别再进行顺序扫描,进而得到3的个数,这样最坏的情况下时间复杂度仍然是O(n),和直接顺序扫描的效率相同。 因此,需要考虑怎样更好的利用二分查找算法,由于数组有序,如果知道了第一个k出现的位置和最后一个k出现的位置,那么我们就可以直接算出有多少个k。因此将思路转化为通过二分查找求第一个和最后一个k出现的位置。 以第一个k出现的位置为例,利用二分查找算法可以直接对数组进行二分,而每次总是拿中间的数字和k做比较,如果中间的数字大于k,那么第一个k只有可能出现在左边,下一次直接在数组左半段继续进行二分查找;如果中间的数字小于k,则第一个k只有可能出现在右边,则在右半段再查找;如果中间的数字等于k,我们先判断它前面的一个数字是不是k,如果不是,那么这个中间的数字就是第一个出现的位置,反之,如果中间数字前面的数字是k,那么第一个k仍然在前半段,继续查找。 同理,找最后一个k出现的位置方法类似,可以使用两个函数分别获得。 编程实现(Java): 题目描述: 一个整型数组里除了两个数字之外,其他的数字都出现了两次****(成对出现,则两个相同的数异或)。请写程序找出这两个只出现一次的数字。要求时间复杂度为O(n),空间复杂度为O(1)。 解题思路: 这道题目相对比较难,一般情况下,我们首先可以想到的是顺序扫描数组,但其时间复杂度为O(n^2)。进一步也可以想到用哈希表保存每一个数次出现的次数,但是这使用了辅助空间,空间复杂度为O(n)。显然均不满足题目要求。 我们先来看一个比较简单的情况,如果数组中只有一个数字出现一次,其他都出现两次。那么我们应该可以想到异或运算。异或运算有一个比较好的性质是:相同为0,相异为1。也就是说, 任何一个数字异或它自己都等于0,而0异或任何数都等于那个数。 因此,我们从头到尾依次异或数组中的每个数字,那么最终结果刚好是那个只出现一次的数字,重复的数字在异或过程中被抵消了。 这是一种比较巧妙的思路,然而,本题只出现一次的数字有两个,简单的异或无法解决。但是,借助这种思路,我们可以进一步分析,如果我们能把数组分成两个子数组,使每个子数组包含一个只出现一次的数字,而其他数字成对出现,那么我们通过上述解法就可以找到两个元素。 具体思路是:我们首先仍然从前向后依次异或数组中的数字,那么得到的结果是两个只出现一次的数字的异或结果,其他成对出现的数字被抵消了。由于这两个数字不同,所以异或结果肯定不为0,也就是这个异或结果一定至少有一位是1,我们在结果中找到第一个为1的位的位置,记为第n位。接下来, 以第n位是不是1为标准,将数组分为两个子数组, 第一个数组中第n位都是1,第二个数组中第n位都是0。这样,便实现了我们的目标。最后,两个子数组分别异或则可以找到只出现一次的数字。 举例: 以{2,4,3,6,3,2,5,5}为例: 我们依次对数组中的每个数字做异或运行之后,得到的结果用二进制表示是0010。异或得到结果中的倒数第二位是1,于是我们根据数字的倒数第二位是不是1分为两个子数组。第一个子数组{2,3,6,3,2}中所有数字的倒数第二位都是1,而第二个子数组{4,5,5}中所有数字的倒数第二位都是0。接下来只要分别两个子数组求异或,就能找到第一个子数组中只出现一次的数字是6,而第二个子数组中只出现一次的数字是4。 题目描述: 在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 解题思路: 题目描述: 题目描述: 解题思路: 第一种情况比较简单。对于第二种情况,有以下两种解法: 时间复杂度为O(n^2)的解法: 点击我看这篇博客,它原来写的不好理解! 输入一个字符串,打印出该字符串中字符的所有排列。例如:输入字符串abc,则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab、cba。 思路: 面对这样的题目,我们需要将复杂问题分解化,分解成一个一个小问题。将一个字符串分为两部分:第一部分为它的第一个字符,第二部分为后面所有的字符,如下图所示: 求整个字符串的全排列,可以看成两步:第一步首先求所有可能出现在第一个位置的字符,即把第一个字符和后面所有的字符交换,上图就是分别把第一个字符a和后面的b、c等字符交换的情形;第二步固定第一个字符,求后面所有字符的排列。这时候仍然把后面的字符分成两部分,后面的第一个字符,和这个字符之后的所有字符,然后把后面的第一个字符和它后面的字符交换。 注:(a)把字符串分成两部分,一部分是字符串的第一个字符,另一部分是第一个字符以后的所有字符(有阴影背景的区域)。接下来我们求阴影部分的字符串的排列。(b)拿第一个字符和它后面的字符逐个交换。 代码实现: 题目描述: 在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。 解题思路: 对于本题,这里给出以下三种解法:(1)用 HashMap 建立每个字符与其出现次数的映射,然后再依次遍历字符串时,找到第一个出现次数为1的字符,返回其位置即可。(2)更进一步,因为该字符串全部是字母,所以可以用一个数组代替哈希表,数组下标就代表该字母。(3)使用模式匹配从前(indexOf)和从后(lastIndexOf)匹配每一个字符,相等即为唯一。 编程实现(Java): 题目描述: 汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! 解题思路: 对于本题,从最直观的角度我们首先可以想到暴力解法:每次移动一位,移动k次为止。对于每一次移动,其实就是将字符串第一个字符放到字符串末尾,而为了实现这一目标,需要将字符串其他位置的元素依次前移,因此,暴力解法时间复杂度为O(n^2)。 还是那句话:最容易想到的解法往往不是最优的。 进一步考虑,字符串左移k位,就相当于将字符串分为两部分,第一部分是前k位,另一部分是剩余的其他位,然后将这两部分交换顺序即可得到最后结果。因此,我们可以得到以下的三次反转算法: 将字符串分为两部分,即前k个字符和剩余的其他字符,然后分别对这两部分进行反转,然后再对整个字符串进行一次反转,这样得到的结果就是我们想要的循环左移之后的字符串。事实上,这并不难理解,前后两部分各自经历了两次反转,因此每一部分的顺序并没有改变,只是将前后两部分进行了交换。对字符串进行一次反转,需要一次扫描,因此次算法时间复杂度为O(n)。 举例: 输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab"; 第一步:翻转字符串“ab”,得到"ba"; 第二步:翻转字符串"cdefg",得到"gfedc"; 第三步:翻转字符串"bagfedc",得到"cdefgab"; 编程实现(Java): 题目描述: 牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? 解题思路: 本题相对比较简单,但是在面试中经常遇到,流传甚广。其主要思路也简洁明了,主要分为以下两步: 第一步:反转整个序列中所有的字符,这时会发现不但反转了单词的顺序,单词中的字母顺序也被反转,因此需要第二步的调整。 第二步:以空格为分隔,依次反转每个单词,即让每个单词会到原来的正常顺序。 举例: 以字符串“student. a am I”为例:整体反转后变为:“I ma a ,tenduts”,然后再反转每个单词,可以得到最后结果:“I am a student.”。 编程实现(Java): 题目描述: 将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 输入输出描述: 输入一个字符串,包括字母、数字、符号,可以为空。如果是合法的数值表达则返回该数字,否则返回0。 解题思路: 本题解决起来并不困难,功能实现简单,但是主要的问题是能否把各种不同的特殊情况都考虑进去,也就是代码的鲁棒性和思考的全面,比如空指针、空字符串、正负号、溢出等等问题。 主要需要注意的点有以下几个: 字符串是否为null或者字符串是否为空字符串。 编程实现(Java): 题目描述: 请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。 输出描述: 如果当前字符流不存在只出现一次的字符,返回“#”字符。 解题思路: 本题还是相当简单的,有点类似于第34题:第一个只出现一次的字符,只不过本题是字符流序列。解题思路也比较类似,将字节流保存起来,通过哈希表统计每个字符出现的次数,然后再从头遍历字符流,找到第一个次数为1的字符,就是我们要找的目标。 这里,为了简单,可以用数组代替哈希表,将字符的ASCLL码作为数组下标,字符对应出现的次数作为数组的元素进行保存。 编程实现(Java): 题目描述: 输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。 解题思路: (三种方法:借助栈、递归、列表的首位插入) 从头到尾打印链表比较简单,从尾到头很自然的可以想到先将链表进行反转,然后再打印。但是,通常我们不希望改变原链表的结构,这是一个只读操作。 因此,我们进一步分析,可以发现排在后面的先输出,这是一个典型的“后入先出”的思想,因此很自然的可以想到用栈来实现,每遍历一个结点,可以将其压入栈中,遍历结束后再逐个弹栈,将结点值存入ArrayList,这样就实现了从尾到头的打印。 更进一步,既然想到了用栈,那一定可以通过递归来实现。每访问到一个结点,先递归输出其后的结点,在输出该结点自身即可。 另外,当我们使用Java或者python语言时,有一种比较巧妙的方法就是使用列表的插入方法,每次插入数据,都总是插入到首位,这样得到的List就是从尾到头的链表序列。 编程实现(Java):(方法三) 题目描述: 输入一个链表,输出该链表中倒数第k个结点。为了符合习惯,从1开始计数,即链表的尾结点是倒数第1个节点。例如,一个链表有6个结点,从头结点开始,它们的值依次是1,2,3,4,5,6。则这个链表倒数第三个结点是值为4的结点。 解题思路: 对于单链表来说,没有从后向前的指针,因此一个直观的解法是先进行一次遍历,统计出链表中结点的个数n,第二次再进行一次遍历,找到第n-k+1个结点就是我们要找的结点,但是这需要对链表进行两次遍历。 为了实现一次遍历,我们这里采用双指针解法。我们可以定义两个指针,第一个指针从链表的头指针开始先向前走k步,第二个指针保持不动,从第k+1步开始,第二个指针也从头开始前进,两个指针都每次前进一步。这样,两个指针的距离都一直保持在k,当快指针(走在前面的)到达null时,慢指针(走在后面的)正好到达第k个结点。注意:要时刻留意空指针的判断。 题目描述: 输入一个链表,反转链表后,输出新链表的表头。 解题思路: 本题比较简单,有两种方法可以实现:(1)三指针。使用三个指针,分别指向当前遍历到的结点、它的前一个结点以及后一个结点。将指针反转后,三个结点依次前移即可。(2)递归方法。同样可以采用递归来实现反转。将头结点之后的链表反转后,再将头结点接到尾部即可。 编程实现(Java):(方法二) 题目描述: 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 解题思路: 首先需要判断几个特殊情况,即判断输入的两个指针是否为空。如果第一个链表为空,则直接返回第二个链表;如果第二个链表为空,则直接返回第一个链表。如果两个链表都是空链表,合并的结果是得到一个空链表。 两个链表都是排序好的,我们只需要从头遍历链表,判断当前指针,哪个链表中的值小,即赋给合并链表指针,剩余的结点仍然是排序的,所以合并的步骤和之前是一样的,所以这是典型的递归过程,用递归可以轻松实现。 题目描述: 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)。 解题思路: 本题有以下三种解法: 第一种:先按照next复制,然后依次添加random指针,添加时需要定位random的位置,定位一次需要一次遍历,需要O(n^2)的复杂度。 第二种:先按照next复制,然后用一个hashmap保存原节点和复制后节点的对应关系,则用O(n)的空间复杂度使时间复杂度降到了O(n)。 第三种(最优方法):同样先按next复制,但是把复制后的节点放到原节点后面,则可以很容易的添加random,最后按照奇偶位置拆成两个链表,时间复杂度O(n),不需要额外空间。 题目描述: 输入两个链表,找出它们的第一个公共结点。 解题思路: 本题首先可以很直观的想到蛮力法,即对链表1(假设长度为m)的每一个结点,遍历链表2(假设长度为n),找有没有与其相同的结点,这显然复杂度为O(mn)。 进一步考虑,我们可以得到以下三种改进的解法: 方法一:借助辅助栈。我们可以把两个链表的结点依次压入到两个辅助栈中,这样两个链表的尾结点就位于两个栈的栈顶,接下来比较两个栈顶的结点是否相同。如果相同,则把栈顶弹出继续比较下一个,直到找到最后一个相同的结点。此方法也很直观,时间复杂度为O(m+n),但使用了O(m+n)的空间,相当于用空间换区了时间效率的提升。 方法二:将两个链表设置成一样长。具体做法是先求出两个链表各自的长度,然后将长的链表的头砍掉,也就是长的链表先走几步,使得剩余的长度与短链表一样长,这样同时向前遍历便可以得到公共结点。时间复杂度为O(m+n),不需要额外空间。 方法三:将两个链表拼接起来。 将两个链表进行拼接,一个链表1在前链表2在后,另一个链表2在前链表1在后,则合成的两个链表一样长,然后同时遍历两个链表,就可以找到公共结点,时间复杂度同样为O(m+n)。 题目描述: 给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。 解题思路: 本题是一个比较典型的链表题目,难度适中。首先,对于大多人来说,看到这道题是比较开心的,因为判断一个链表是否存在环的方法,基本上大家都知道,就是快慢指针法,但是再仔细一看,本题除了判断是否有环之外,还要找到这个环的入口点,这就略有些复杂了。 具体思路如下: 第一步:确定一个链表是否有环。这一步就是快慢指针法,定义两个指针,同时从链表的头结点出发,快指针一次走两步,慢指针一次走一步。如若有环,两个指针必定相遇,也就是如果快指针反追上了慢指针,说明存在环(这里要注意,两指针相遇的地方一定在环中,但不一定是环的入口),如果快指针走到了链表的末尾(指向了NULL),则说明不存在环。 第二步:找到环的入口点。这还是可以利用双指针来解决,两个指针初始都指向头结点,如果我们可以知道环中的结点个数,假设为n,那么第一个指针先向前走n步,然后两个指针(另一个从头结点开始)同时向前,当两个指针再次相遇时,他们的相遇点正好就是环的入口点。 这其实并不难理解,假设链表中共有m个结点,环中有n个结点,那么除环以外的结点数就是m-n,第一个指针先走了n步,然后两个指针一起向前,当他们一起向前m-n步时,第一个链表正好走完一遍链表,返回到环的入口,而另一个指针走了m-n步,也正好是到了环的入口。 现在,我们还有一个关键的问题:如何知道链表中的环包含了几个结点呢?也就是,怎么求这个n。 实际上这也不难,在第一步中,我们提到:快慢指针相遇的地方一定在环中,并且通过第一步我们已经找到了这个位置,接下来,只要从这个相遇的结点出发,一边移动一边计数,当它绕着环走一圈,再次回到这个结点时,就可以得到环中的结点数目n了。 题目描述: 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。 解题思路: 关于链表的大多数题目还是比较简单的,本题也并不太难。 删除重复结点,也就是如果当前结点和下一个结点的值相同,那么就是重复的结点,都可以被删除,为了保证删除之后的链表的连通性,在删除之后,要把当前结点前面的结点和下一个没有重复的结点链接起来,为此,程序需要记录当前的最后一个不重复结点,即程序中的pre。重点在于:一定要确保当前链接到链表中的一定是不会再重复的结点,具体见代码实现。 关于第一个结点如果重复怎么办的问题,我们不用单独考虑,可以使用链表中一贯的做法,加一个头结点即可。 具体思路看代码比较直观,参考如下的代码实现。 题目描述: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回根结点。 解题思路: 树的遍历有三种:分别是前序遍历、中序遍历、后序遍历。本题是根据前序和中序遍历序列重建二叉树,我们可以通过一个具体的实例来发现规律,不难发现:前序遍历序列的第一个数字就是树的根结点。在中序遍历序列中,可以扫描找到根结点的值,则左子树的结点都位于根结点的左边,右子树的结点都位于根结点的右边。 这样,我们就通过这两个序列找到了树的根结点、左子树结点和右子树结点,接下来左右子树的构建可以进一步通过递归来实现。 举例: 题目描述: 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) 解题思路: 要查找树A中是否存在和树B结构一样的子树,我们可以分为两步:第一步,在树A中找到和树B的根结点值一样的结点R;第二步,判断树A中以R为根结点的子树是不是包含和树B一样的结构。 对于这两步,第一步实际上就是树的遍历,第二步是判断是否有相同的结构,这两步都可以通过递归来实现。 题目描述: 操作给定的二叉树,将其变换为原二叉树的镜像。 解题思路: 求一棵树的镜像的过程:先前序遍历这棵树的每个结点,如果遍历到的结点有子结点,就交换它的两个子结点。当交换完所有的非叶结点的左、右子结点后,就可以得到该树的镜像。 如下面的例子,先交换根节点的两个子结点之后,我们注意到值为10、6的结点的子结点仍然保持不变,因此我们还需要交换这两个结点的左右子结点。做完这两次交换之后,我们已经遍历完所有的非叶结点。此时变换之后的树刚好就是原始树的镜像。 题目描述: 从上往下打印出二叉树的每个节点,同层节点从左至右打印。 解题思路: 本题实际上就是二叉树的层次遍历,深度遍历可以用递归或者栈,而层次遍历很明显应该使用队列。同样我们可以通过一个例子来分析得到规律:每次打印一个结点时,如果该结点有子结点,则将子结点放到队列的末尾,接下来取出队列的头重复前面的打印动作,直到队列中所有的结点都打印完毕。 题目描述: 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 解题思路: 对于后续遍历序列,序列的最后一个值一定是树的根结点,而由二叉搜索树的性质:左小右大,我们可以从头开始遍历,当遍历到某个值比根结点大时停止,记为flag,此时flag之前的所有数值都是二叉搜索树的左子树的结点,flag以及flag之后的所有数都是二叉搜索树的右子树的结点。这是由二叉搜索树以及后序遍历共同决定的。 接下来,我们就可以把任务交给递归,同样的方法去判断左子树和右子树是否是二叉搜索树,这显然是典型的递归解法。 举例: 以{5,7,6,9,11,10,8}为例,后序遍历结果的最后一个数字8就是根结点的值。在这个数组中,前3个数字5、7和6都比8小,是值为8的结点的左子树结点;后3个数字9、11和10都比8大,是值为8的结点的右子树结点。 我们接下来用同样的方法确定与数组每一部分对应的子树的结构。这其实就是一个递归的过程。对于序列5、7、6,最后一个数字6是左子树的根结点的值。数字5比6小,是值为6的结点的左子结点,而7则是它的右子结点。同样,在序列9、11、10中,最后一个数字10是右子树的根结点,数字9比10小,是值为10的结点的左子结点,而11则是它的右子结点,所以它对应的二叉搜索树如下: 题目描述: 输入一颗二叉树的根结点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前) 解题思路: 本题实质上就是深度优先搜索。使用前序遍历的方式对整棵树进行遍历,当访问到某一个结点时,将该结点添加到路径上,并且累加该结点的值。当访问到的结点是叶结点时,如果路径中的结点值之和正好等于输入的整数,则说明是一条符合要求的路径。如果当前结点不是叶结点,则继续访问它的子结点。 当找到一条符合要求的路径之后,需要回溯进一步查找别的路径,因此,这实际上仍然是一个递归的过程,特别注意在函数返回之前要删掉当前结点,从而才可以正确的回溯。 题目描述: 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 解题思路: 首先要理解此题目的含义,在双向链表中,每个结点都有前后两个指针;二叉树中,每个结点都有两个指向子结点的左右指针,同时,二叉搜索树树也是一种排序的数据结构。因此,从结构上看,双向链表的前后指针和二叉搜索树的左右指针结构相似,因此,可以实现互相之间的转换。 首先,根据二叉搜索树的特点,左结点的值<根结点的值<右结点的值,据此不难发现,使用二叉树的中序遍历得到的数据序列就是递增的排序顺序。因此,首先确定应该采用中序遍历方法。 接下来,可以根据下图,将树分为三个部分,值为10的根结点、根为6的左子树和根为14的右子树。不难看出以下规律:根据中序遍历的顺序,当我们遍历到根结点时,它的左子树已经转换为一个排好序的双向链表,并且链表最后一个结点是左子树值最大的结点,我们把这个值最大(8)的结点同根结点链接起来,10就成了最后一个结点,接着遍历右子树,将根结点同右子树中最小的结点链接起来。 题目描述: 输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 解题思路: 本题相对比较简单。根据二叉树深度的定义,我们有以下理解:如果一棵树只有一个结点,那么它的深度为1。如果根结点只有左子树而没有右子树,那么树的深度为其左子树深度加1;相反,如果根结点只有右子树而没有左子树,那么深度为右子树深度加1;如果既有左子树又有右子树,那么该树的深度为左、右子树深度的较大值加1。 因此,很明显本题应该使用递归的思路来解决。 题目描述: 输入一棵二叉树,判断该二叉树是否是平衡二叉树。这里的定义是:如果某二叉树中任意结点的左、右子树的深度相差不超过1,那么它就是一棵平衡二叉树。 解题思路: 首先对于本题我们要正确理解,一般情况下,平衡二叉树就是AVL树,它首先是二叉搜索树(左小右大),其次满足左右子树高度之差不超过1。但是在本题中,没有二叉搜索树的要求,只对平衡与否进行判断即可。 根据求二叉树深度的思路我们很容易想到一种解法,即:在遍历树的每一个结点时,求其左右子树的深度,判断深度之差,如果每个结点的左右子树深度相差都不超过1,那么就是一棵平衡二叉树。本思路直观简洁,但是需有很多结点需要重复遍历多次,时间效率不高。 为了避免重复遍历,我们可以得到一种 每个结点只遍历一次的解法。 思路如下:采用后序遍历的方式遍历二叉树的每个结点,这样在遍历到每个结点的时候就已经访问了它的左右子树。所以,只要在遍历每个结点的时候记录它的深度,我们就可以一边遍历一边判断每个结点是不是平衡的。 编程实现(Java): 题目描述: 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 解题思路: 本题解决起来并不是很困难,主要是分析清楚所有可能的不同情况。对于中序遍历序列来说,遵循“左->根->右”的顺序,在深刻理解中序遍历的基础上,结合一些具体的实例我们不难得出以下结论。 找一个结点在中序遍历序列中的下一个结点共有三种不同的情况: 如果一个结点有右子树,那么它的下一个结点就是它的右子树中最左边的那个结点,也就是说,从它的右子结点出发一直访问左指针,最后就可以找到这个最左结点。 对应第一种情况(有右子树的情形),例如,图中结点b的下一个结点是h,结点a的下一个结点是f。 对应第二种情况(没有右子树,但是其父结点的左结点的情形),例如,图中结点d的下一个结点是b,f的下一个结点是c。 对应第三种情况(没有右子树,但是是其父结点的右结点),例如,为了找到结点g的下一个结点,我们沿着指向父结点的指针向上遍历,先到达结点c。由于结点c是父结点a的右结点,我们继续向上遍历到达结点a。由于结点a是树的根结点。它没有父结点。因此结点g没有下一个结点。 编程实现(Java): 题目描述: 请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 解题思路: 本题判断一棵树是不是对称的,和第18题可以对比分析:二叉树的镜像,和LeetCode第101题:101. 对称二叉树是同一道题。 解法一:递归法 《剑指Offer》中给出的解答是定义一种先遍历右子结点,再遍历左子结点的新遍历方法,称为前序遍历的对称遍历,实际上,我们不用这样,可以直接找到对称二叉树的规律: 对称二叉树满足:根结点的左右子树相同,左子树的左子树和右子树的右子树相同,左子树的右子树和右子树的左子树相同即可。 根据以上规律,直接使用递归便可以写出对应的代码,并不难理解,可以结合以下几个实例进行分析。 解法二:迭代法(广度优先遍历) 广度优先遍历的一般做法是借助队列,这里我们可以在初始化时把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。 这一方法的关键是队列中出队列的两个连续的结点就应当是对称树中相等的结点。 题目描述: 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 解题思路: 这道题仍然是二叉树的遍历,相当于层次遍历,可以和第22题:从上往下打印二叉树 和第60题:把二叉树打印成多行 这几个题对比起来进行分析。 相对而言,本题按之字形顺序打印二叉树是比较复杂的,短时间内不太好分析得到结论,可以通过具体的实例来进行分析,从具体的例子得出普遍的结论。 题目描述: 从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。 解题思路: 本题可类比第22题:从上往下打印二叉树,这两道题实际上是一回事,只不过这里我们多了一个分行打印的要求,实际上大同小异,稍加修改即可。 在二叉树层次遍历上,我们使用的是队列,借助队列先进先出的性质实现,具体规律:每次打印一个结点时,如果该结点有子结点,则将子结点放到队列的末尾,接下来取出队列的头重复前面的打印动作,直到队列中所有的结点都打印完毕。在此基础上我们考虑这里的分行要求,不难想到我们只要增加两个变量即可:一个用于保存当前层中还没有打印的结点个数,另一个用于记录下一层结点的数目。而使用队列的话,实际上这两个变量可以统一用队列的长度来实现。 题目描述: 给定一棵二叉搜索树,请找出其中的第k小的结点。例如(5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。 解题思路: 本题实际上比较简单,主要还是考察对树的遍历的理解,只要熟练掌握了树的三种遍历方式及其特点,解决本题并不复杂,很明显本题是对中序遍历的应用。 对于本题,我们首先可以知道二叉搜索树的特点:左结点的值<根结点的值<右结点的值。因此,我们不难得到如下结论:如果按照中序遍历的顺序对一棵二叉搜索树进行遍历,那么得到的遍历序列就是递增排序的。因此,只要用中序遍历的顺序遍历一棵二叉搜索树,就很容易找出它的第k大结点。 举例: 题目描述: 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。假设n<=39。 解题思路: 斐波那契数列:0,1,1,2,3,5,8… 总结起来就是:第一项是0,第二项是1,后续第n项为第n-1项和第n-2项之和。 用公式描述如下: 因此,可以不使用递归,直接使用简单的循环方法实现。 题目描述: 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。 解题思路: 首先考虑最简单的情况,如果只有1级台阶,显然只有一种跳法。如果有两级台阶,就有两种跳法:一种是分两次跳,一种是一次跳两级。 在一般情况下,可以把n级台阶的跳法看成n的函数,记为f(n),那么一般情况下,一开始我们有两种不同的选择:(1)第一步只跳一级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即f(n-1);(2)第一步跳两级,那么跳法数目等于后面剩下的n-2级台阶的跳法数目,即f(n-2)。所以f(n)=f(n-1)+f(n-2)。 至此,我们不难看出本题实际上就是求斐波那契数列,直接按照第7题思路便可以解决。 编程实现(Java): 题目描述: 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 解题思路: 当只有一级台阶时,f(1)=1;当有两级台阶时,f(2)=f(2-1)+f(2-2);一般情况下,当有n级台阶时,f(n)=f(n-1)+f(n-2)+···+f(n-n)=f(0)+f(1)+···+f(n-1),同理,f(n-1)=f(0)+f(1)+···+f(n-2). 因此,根据上述规律可以得到:f(n)=2*f(n-1)。这时一个递推公式,同样为了效率问题,用循环可以实现。 编程实现(Java): 题目描述: 我们可以用2 X 1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2 X 1的小矩形无重叠地覆盖一个2 X n的大矩形,总共有多少种方法? 解题思路: 我们可以以2 X 8的矩形为例。 先把2X8的覆盖方法记为f(8),用1X2的小矩形去覆盖时,有两种选择:横着放或者竖着放。当竖着放时,右边还剩下2X7的区域。很明显这种情况下覆盖方法为f(7)。当横着放时,1X2的矩形放在左上角,其下方区域只能也横着放一个矩形,此时右边区域值剩下2X6的区域,这种情况下覆盖方法为f(6)。所以可以得到:f(8)=f(7)+f(6),不难看出这仍然是斐波那契数列。 特殊情况:f(1)=1,f(2)=2 编程实现(Java): 题目描述: 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 解题思路: 本题看似比较简单,是一个简单的指数运算,但需要完整的考虑到所有情况。首先,对于底数,如果底数为0,则0的任何次方都是0,可以直接返回0。关键在于指数,指数可能有三种情况,有可能是正数、0、负数。对于指数是0的情况,任何数的0次方为1。对于指数是负数的情况,可以将其转化为绝对值计算,求出结果之后再求倒数。 在计算n次方的时候,为了方便,我们根据减治的思想,通过同底数指数幂的公式计算,如下列公式。这里需要注意的是奇数和偶数的不同。 题目描述: 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 解题思路: 本题本身没有太多的实际意义,但是可以对程序员的发散思维能力进行考察,进而可以反映出对编程相关技术理解的深度。 对于本题,书中给出了利用构造函数、虚函数、函数指针、模板类型求解等思路,在这里,若使用java实现,有些方法却是不适用的,比如构造函数法,java构造对象数组并不会多次调用构造函数,其他方法略显复杂,这里我们给出另外一个思路: 可以通过递归来实现加法,但是由于无法使用if语句,因此对于递归的结束条件无法进行判断,这里用一个比较巧妙的思路:与运算的短路特性,所谓短路,比如 A && B,当A条件不成立时,不论B是否成立,结果都是false,所以B不再进行计算,利用短路特性可以实现递归停止,进而求出和。 编程实现(Java): 题目描述: 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 解题思路: 本题同样是对发散思维能力的一个考察。首先,我们需要考虑是要求和却不能使用四则运算,那么还能用什么呢?除了四则运算以外,还可以进行计算的也就只剩下了位运算。因此,需要进一步考虑二进制数的位运算,用位运算来代替加法。 具体思路是:三步走策略。第一步,不考虑进位对每一位相加(模2和),也就是0+0=0,1+1=0,0+1=1,1+0=0,不难看出这一步其实就是做异或运算。第二步,考虑进位,只有1+1会产生进位,因此求每一位的进位可以先将两个数做与运算,然后再左移一位。第三步,将前面两个结果相加,相当于递归相加,直到不产生进位为止。 编程实现(Java): 题目描述:编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为 汉明重量).)。
若mn
根据上述规则,我们需要先把数字转换成字符串再进行比较,因为需要拼接起来。比较完之后按顺序连接成一个字符串即可。
①用自定义规则排好序
②然后把排序结果丢到result就可以了import java.util.Arrays;
import java.util.Comparator;
public class Practise_07 {
public static void main(String[] args) {
int[] array = new int[]{3,2,1};
System.out.println("原来的数组:"+Arrays.toString(array));
System.out.println("处理后的数:"+PrintMinNumber(array));
}
private static String PrintMinNumber(int[] array) {
//result用来接收结果,即最小的数
String result = "";
//判空
if(array==null || array.length==0) {
return result;
}
//用str数组来接收整数数组的数
String[] str = new String[array.length];
for(int i=0;i<array.length;i++) {
str[i] = String.valueOf(array[i]);
}
//因为数据是引用类型数据不是基本数据类型,因此这里的排序需要用自定义的规则来排序,自定义规则需要重写Compare方法
Arrays.sort(str,new Comparator<String>(){
public int compare(String str1,String str2) {
String c1 = str1+str2;
String c2 = str2+str1;
return c1.compareTo(c2);
}
});
//把从小到大排好序的str数组的数逐一加到result中
for(String s:str) {
result+=s;
}
return result;
}
}
8、《剑指offer》35 数组中的逆序对 √
如2431中,21,43,41,31是逆序,逆序数是4,为偶排列。
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
解题思路:public class Solution {
int result=0;
public int inversePairs(int [] array) {
//数组中的逆序对,归并排序
if(array==null || array.length==0) //因为查找的是多少对逆序对,所以如果数组为空,则返回的是0对
return 0;
findInversePairs(array,0,array.length-1);
return result%1000000007;
}
public void findInversePairs(int[] array,int low,int high){
if(low<high){
int mid=low+(high-low)/2;
findInversePairs(array,low,mid); //左一半递归
findInversePairs(array,mid+1,high); //右一半递归
merge(array,low,mid,high); //合并merge
}
}
public void merge(int[] array,int low,int mid,int high){
int i=low,j=mid+1;
int k=0; //k是下面定义数组的索引
int[] temp=new int[high-low+1];
while(i<=mid && j<=high){
if(array[i]<=array[j])
temp[k++]=array[i++];
else { //大于,说明是逆序
temp[k++] = array[j++];
result+= (mid - i + 1); //这是因为,左边的数组是从小到大排的,如果前面的i比后面的j对应的元素大,则i到mid的所有元素都比j要大。(如果第一个序列当前值大于第二个序列当前值,则有第一个序列“当前长度”个逆序对。)因为左边的数组,是从小到大排的,如果第一个比后面的大,则第二个数也会比后面的大,所以有第一个序列“当前长度”个逆序对。
result= result%1000000007;
}
}
while(i<=mid) //比较完之后,左一半递归还剩下的元素,丢到temp数组中
temp[k++]=array[i++];
while(j<=high) //比较完之后,右一半递归还剩下的元素,丢到temp数组中
temp[k++]=array[j++];
for(i=0;i<temp.length;i++) //这里是归并排序的操作,将temp中的元素全部拷贝到原数组中
array[low+i]=temp[i];
}
}
9、《剑指offer》37 统计数字在排序数组中出现的次数 √
//看自己写的
public class shuzu_08 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3,3,3,4};
int first = fing_first_key(array,3);
int last = find_last_key(array,3);
int number = last - first + 1;
System.out.println(first);
System.out.println(last);
System.out.println(number);
}
//找第一个K
public static int fing_first_key(int[] array,int key) {
if(array == null || array.length == 0) {
return -1;
}
int low = 0;
int high = array.length-1;
while(low <= high) {
int mid = (low + high) / 2;
if(array[mid] < key) {
low = mid + 1;
}else if(array[mid] > key) {
high = mid -1;
}else {
mid = mid -1;
if(array[mid] == key) {
high = mid;
}else {
return mid + 1;
}
}
}
return -1;
}
//找最后一个K
public static int find_last_key(int[] array,int key) {
if(array == null || array.length == 0) {
return -1;
}
int low = 0;
int high = array.length-1;
while(low <= high) {
int mid = (low + high) / 2;
if(array[mid] < key) {
low = mid + 1;
}else if(array[mid] > key) {
high = mid -1;
}else {
mid = mid + 1;
if(array[mid] == key) {
low = mid;
}else {
return mid - 1;
}
}
}
return -1;
}
}
import java.util.Arrays;
public class Practise_09 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3,3,3,4,5};
int times = getNumberOfK(array,3);
System.out.println("原数组:"+Arrays.toString(array));
System.out.println("3出现的次数:"+times);
}
//查找第一个K,和最后一个K,返回二者下标相减+1,即k有多少个
public static int getNumberOfK(int[] array,int k ) {
int first = getFirstNumber(array,k);
int last = getLastNumber(array,k);
if(first == -1 || last == -1 ) {
return 0;
}
return last-first+1;
}
//查找第一个K出现时的下标
public static int getFirstNumber(int[] array,int k) {
int result = -1;
if(array==null || array.length==0) {
return result ;
}
int low = 0,high = array.length-1;
while(low <= high) {
int mid = low+(high-low)/2;
if(array[mid]<k) { //小于 K
low = mid+1;
}
else if(array[mid]>k) { //大于 K
high = mid - 1;
}
else{ //等于 K的时候,因为我们要找的是第一个k,所以不确定中间的数是不是第一个,所以还要怕都难它前面的一个数
mid = mid - 1;
if(mid<low || array[mid]!=k) {
return mid+1;
}
else {
high = mid;
}
}
}
return result;
}
//查找最后一个K出现时的下标
public static int getLastNumber(int[] array,int k) {
int result = -1;
if(array==null || array.length==0) {
return result ;
}
int low = 0,high = array.length-1;
while(low <= high) {
int mid = low+(high-low)/2;
if(array[mid]<k) {
low = mid+1;
}
else if(array[mid]>k) {
high = mid - 1;
}
else{ //上面的函数和这个函数唯一的区别是这里开始
mid = mid + 1;
if(mid>high || array[mid]!=k) {
return mid-1;
}
else {
low = mid;
}
}
}
return result ;
}
}
10、《剑指offer》40 如何找到数组中只出现过一次的数字 √
public class Solution {
public int[] FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
/*
思路:数组中的元素先依次异或,相同为0,则得到的是两个只出现一次的数的异或结果
(因为其他数都是成对出现,所以在异或过程中抵消了)对于得到的异或结果,找到其第
一个为1的位,找到后就可以根据这个位,将两个不同的数分到两个不同的数组,这就找到了。
该位为1,说明两个只出现一次的数该位不同,所以按照该位是0还是1将数组分成两部分
这样,而两个只出现一次的数正好被分开,再各自异或可得结果。
*/
int result=0;
if(array==null||array.length<2)
return result;
for(int num:array) //数组中的元素先依次异或,相同为0,则得到的是两个只出现一次的数的异或结果
result ^= num; //因为数组中其他的数都是成对出现,在异或过程中抵消了,而相同的数异或为0,0跟其他数异或为其他数
//找到两个不相同的数的异或结果result中第一个为1的位,即找到index的位置,因为在这个位置两个数不同【因为“相同为0,相异为1”,而该位为1,说明两个只出现一次的数该位不同,所以按照该位是0还是1将数组分成两部分】
int index=0;
for(;index<32;index++){
if(((result>>index) & 1)==1) //一开始右移0位,然后右移1位,一位一位的找
break;
}
//num1,num2分别为长度为1的数组。分别用来保存两个不同的数,作为返回结果。
//将num1[0],num2[0]设置为返回结果
int[] res = new int[2];
/*按照该位是0还是1将数组分成两部分,分别异或,由于两个出现一次的数在index位上不一样,则可以根据该位将
这两个不同的数放在两个不同的数组上,而由于除这两个数之外的数都是成对出现,在异或过程中被抵消了,所以
不用管其他的数。
*/
for(int num:array){
if(((num>>index)&1)==1)
res[0] ^= num;
else
res[1] ^= num;
}
return res;
}
}
11、《剑指offer》50 如何找到数组中的重复数字 √
思路一:循环两次一个个去算有没重复,时间复杂度O(n2)
思路二:先排号序,然后遍历搜索重复的数字O(logn)
思路三:空间换时间,时间复杂度O(n)+空间复杂度O(n)
思路四:时间复杂度O(n)//思路三
public static void test3(int[] a, int len) {
// 这个方法没有对数据的有效性进行检测,有需要的自己添加
// 这个数组默认全为-1
int[] b = new int[len];
for (int i = 0; i < len; i++) {
b[i] = -1;
}
// 循环遍历原数组,如果b[a[i]] 的位置存了数据不为-1的话 说明这个数在之前已经出现过了,所以重复
for (int i = 0; i < len; i++) {
if (b[a[i]] != -1) {
System.out.println(a[i]);
}
b[a[i]] = a[i];
}
// 原理就是将这个数组中的每一个数依次放到对应位置上去,如果之前有了,那么就是重复,如果没有,那就放
}
// 思路四
// 这是我利用java的hashSet实现的
public void test2(int[] a, int len) {
// 这个方法没有对数据的有效性进行检测,有需要的自己添加
HashSet hashSet = new HashSet();
for (int i = 0; i < len; i++) {
boolean add = hashSet.add(a[i]);
if (add == false) {
System.out.print(a[i] + " ");
}
}
System.out.println();
}
12、《剑指offer》51 构建乘积数组 √
import java.util.Arrays;
public class Practise_12 {
public static void main(String[] args) {
int[] A = new int[]{1,2,3,4,5};
int[] B = multiply(A);
System.out.println(Arrays.toString(A));
System.out.println(Arrays.toString(B));
}
public static int[] multiply(int[] A) {
/*
思路:分成两部分的乘积,第一部分可以自上而下,第二部分自下而上
*/
if(A==null||A.length<1)
return A;
int len=A.length;
int[] B=new int[len];
B[0]=1;
//计算左三角
for(int i = 1;i <= len-1;i++) { //为什么从1开始,因为B[0]上面已经算了
B[i] = B[i-1] * A[i-1];
}
//计算右三角 temp用来记录有三角每一行的值
int temp=1;
for(int i = len-2;i >= 0;i--){ //第二部分可以自下而上,为什么从len-2开始,本来是从len-1开始的,但是temp=1就是len-1的值为1嘛,所以从len-2开始
temp=temp * A[i+1];
B[i]=B[i] * temp;
}
return B;
}
}
13、《剑指offer》51 求数组中和为S的两个数字 √
public int[] twoSum(int[] nums, int target) {
if(nums.length == 0 || nums.length == 1) return new int[0];
int[] res = new int[2];
for(int i=0;i<nums.length;i++) {
int a = nums[i];
int low = i;
int high = nums.length-1;
while(low<=high) {
int mid = (low+high)/2 ;
if(nums[mid] == target-a) {
res[0] = a;
res[1] = nums[mid];
return res;
}else if(nums[mid] > target-a){
high = mid -1;
}else {
low = mid+1;
}
}
}
return res;
}
14、《剑指offer》按顺序打印出从 1 到最大的 n 位十进制数 √
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
public int[] printNumbers(int n) {
int length = (int)Math.pow(10,n)-1;
int[] res = new int[length];
for(int i=0;i<length;i++) {
res[i] = i+1;
}
return res;
}
15、《剑指offer》0~n-1中缺失的数字 √
public int missingNumber(int[] nums) {
int[] array = new int[nums.length];
for(int i=0;i<nums.length;i++) {
array[i] = i;
}
for(int i=0;i<nums.length;i++) {
if(array[i]!=nums[i])return array[i];
}
return nums.length;
}
二、字符串(共9道题目)
13、《剑指offer》2 把空格替换成其他字符串(替换空格)
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
对于这个题目,我们首先想到原来的一个空格替换为三个字符,字符串长度会增加,因此,存在以下两种不同的情况:(1)允许创建新的字符串来完成替换。(2)不允许创建新的字符串,在原地完成替换。
从头到尾遍历字符串,当遇到空格时,后面所有的字符都后移2个。
时间复杂度为O(n)的解法:
可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,每替换一个空格,长度增加2,即替换之后的字符串长度为原来的长度+2*空格数目。接下来从字符串的尾部开始复制和替换,用两个指针P1和P2分别指向原始字符串和新字符串的末尾,然后向前移动P1,若指向的不是空格,则将其复制到P2位置,P2向前一步;若P1指向的是空格,则P1向前一步,P2之前插入%20,P2向前三步。这样,便可以完成替换,时间复杂度为O(n)。 public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for(Character c : s.toCharArray())
{
if(c == ' ') res.append("%20");
else res.append(c);
}
return res.toString();
}
14、《剑指offer》27 输入一个字符串,打印出该字符串中所有字符的排列(字符串的排序)
题目:
import java.util.*;
public class Practise_14{
public static void main(String[] args) {
String str = "abc";
permutation(str);
}
//给输入的str字符串中的字符进行全排列
public static void permutation(String str){
if(str == null){ //如果字符串为空,直接返回
return ;
}
permutation(str.toCharArray(), 0); //否则将字符串转换为字符数字,并从字符0位置开始进行全排列
}
public static void permutation(char[] chars, int pos) {
if(pos == chars.length - 1){
System.out.println(chars);
}
for(int i = pos; i < chars.length; i++){
//首部字符和它后面的字符(包括自己)进行交换
char temp = chars[i];
chars[i] = chars[pos];
chars[pos] = temp;
//递归求后面的字符的排列
permutation(chars, pos+1);
//由于前面交换了一下,所以chs的内容改变了,我们要还原回来
temp = chars[i];
chars[i] = chars[pos];
chars[pos] = temp;
}
}
}
15、《剑指offer》34 如何找到第一个只出现一次的字符
//方法一:哈希表
public int FirstNotRepeatingChar(String s) {
Map<Character,Integer> map = new HashMap<Character,Integer>();
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
map.put(c,map.getOrDefault(c,0)+1);
}
for(int i=0;i<s.length();i++)
if(map.get(s.charAt(i))==1)
return i;
return -1;
}
//方法二:数组代替哈希表
public int FirstNotRepeatingChar(String str) {
if(str==null || str.length()==0)
return -1;
// A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122
int len=str.length();
int[] count=new int[58]; //122-65+1
for(int i=0;i<len;i++){
char c=str.charAt(i);
count[c-'A']++;
}
for(int i=0;i<len;i++){
char c=str.charAt(i);
if(count[c-'A']==1)
return i;
}
return -1;
}
//方法三:模式匹配
public int firstUniqChar(String s) {
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
if(s.indexOf(c)==s.lastIndexOf(c))
return i;
}
return -1;
}
16、《剑指offer》43 如何左旋转字符串
//方法一,依次左移,每次移动一位
public String LeftRotateString(String str,int n) {
char[] strArr=str.toCharArray();
int len=strArr.length;
if(len<=0)
return str;
n=n%len;
for(int i=0;i<n;i++){ //只控制循环次数
char c=strArr[0];
for(int j=0;j<len-1;j++) //拿出第一个,后面依次前移,复杂度O(n^2)
strArr[j]=strArr[j+1];
strArr[len-1]=c;
}
return new String(strArr);
}
//方法二:三次反转
public String LeftRotateString(String str,int n) {
char[] strArr=str.toCharArray();
int len=strArr.length;
if(len<=0)
return str;
n=n%len;
reverseStr(strArr,0,n-1);
reverseStr(strArr,n,len-1);
reverseStr(strArr,0,len-1);
return new String(strArr);
}
public void reverseStr(char[] array,int begin,int end){ //反转字符串,前后指针
for(;begin<end;begin++,end--){
char c=array[begin];
array[begin]=array[end];
array[end]=c;
}
}
17、《剑指offer》44 如何反转单词序列
class Solution {
public String reverseWords(String s) {
s = s.trim(); // 删除首尾空格
int j = s.length() - 1, i = j;
StringBuilder res = new StringBuilder();
while(i >= 0) {
while(i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
j = i; // j 指向下个单词的尾字符
}
return res.toString().trim(); // 转化为字符串并返回
}
}
18、《剑指offer》49 如何把字符串转化为整数
字符串对于正负号的处理,特别是正号,可能有也可能没有,但都代表正数
输入值是否合法,判断除首位可能为正负号外,其他位是否都是数字
int为32位,最大的整数是刚刚超过21亿,也就是10位十进制数
使用错误标志,区分合法值0和非法值0
以下直接给出相应的代码实现。public class Solution {
public int StrToInt(String str) {
//判断字符串是否为空
if(str==null || str.length()==0)
return 0;
//通过首位进行判断,并标记正负数/是否合法
char c = str.charAt(0); //首位
int flag=0; //标记正负数
boolean isVaild=false; //标记是否合法
if(c=='+') //为正数
flag=1;
else if(c=='-') //为负数
flag=-1;
else if(c>='0'&&c<='9') { //正数,便于统一处理
flag=1;
str="+"+str;
}else { //不是数,不合法
isVaild=true;
return 0;
}
//计算后续数字
int len= str.length();
if(len>11) //最大整数是10位
return 0;
long res=0;
for(int i=1;i<len;i++){
c=str.charAt(i);
if(c<'0'||c>'9'){
isVaild=true;
return 0;
}
res=res*10+(c-'0'); //计算数值大小
}
//根据标志位(flag、isValid)返回值
if(flag==1 && res<=Integer.MAX_VALUE)
return (int)res;
if(flag==-1 && (-1*res)>=Integer.MIN_VALUE)
return (int)(-1*res);
if(isVaild==true)
return 0;
return 0;
}
}
19、《剑指offer》52 正则化表达式匹配
public class Solution {
public boolean match(char[] str, char[] pattern){
/*
思路:比较前两个字符,递归比较
*/
if(str==null || pattern==null)
return false;
return match(str,0,pattern,0);
}
public boolean match(char[] str,int i,char[] pattern,int j){
if(i==str.length && j==pattern.length)//都为空
return true;
if(i<str.length && j==pattern.length)//模式串为空
return false;
//以下j一定是
20、《剑指offer》53 表示数值的字符串
21、《剑指offer》54 如何找到字符流中第一个不重复的字符
public class Solution {
/*
思路:用hashmap保存每个字符出现的次数 或者 用长度为256的数组代替哈希表
还有一种是使用indexof和lastIndexof
*/
String str="";
int[] charToCount=new int[256]; //256个字符
//Insert one char from stringstream
public void Insert(char ch)
{
str+=ch;
charToCount[ch]+=1;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
if(charToCount[c]==1)
return c;
}
return '#';
}
}
三、链表(共8道题目)
22、《剑指offer》3 从尾到头打印链表
//方法一:借助栈的后入先出实现
public ArrayList<Integer> printListFromTailToHead(ListNode listNode){
ArrayList<Integer> list=new ArrayList<>();
if(listNode==null)
return list;
ListNode head=listNode;
Stack<ListNode> stack=new Stack<>();
while(head!=null){ //依次压入栈中
stack.push(head);
head=head.next;
}
while(!stack.isEmpty()){ //逐个弹栈
ListNode temp=stack.pop();
list.add(temp.val);
}
return list;
}
//方法二:递归实现
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list=new ArrayList<>();
getNext(listNode,list);
return list;
}
public void getNext(ListNode listNode,ArrayList<Integer> list){
if(listNode!=null){
getNext(listNode.next,list); //先递归输出其后的结点
list.add(listNode.val); //再输出自身
}
}
//方法三:列表的首位插入
public ArrayList<Integer> printListFromTailToHead(ListNode listNode){
ArrayList<Integer> list=new ArrayList<>();
if(listNode==null)
return list;
ListNode head=listNode;
while(head!=null){
list.add(0,head.val); //每次插入数据,都总是插入到首位
head=head.next;
}
return list;
}
23、《剑指offer》14 如何找到链表中倒数第k个结点
public ListNode FindKthToTail(ListNode head,int k) {
if(head==null)
return null;
//思路:快慢指针,快指针先走k步,慢指针从头开始,都向前进,快指针到null时,前一个正好是倒数第k个
ListNode fast=head,slow=head;
for(int i=0;i<k;i++){ //快指针先走k步
if(fast!=null)
fast=fast.next;
else
return null;
}
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
return slow;
}
24、《剑指offer》15 反转链表
//方法一:三指针
public ListNode ReverseList(ListNode head) {
if(head==null)
return null;
ListNode first=null;
ListNode second=head;
ListNode third=head.next;
while(third!=null){
second.next=first; //三指针之间的变换
first=second;
second=third;
third=third.next;
}
second.next=first;
return second;
}
//方法二:递归
public ListNode ReverseList(ListNode head) {
if(head==null||head.next==null)
return head;
ListNode temp=ReverseList(head.next); //temp一直递归循环,直到temp得到最后一个结点
head.next.next=head; //head指向的是最后一个结点的前一个结点,如果不太懂举3个数为例子,一次次递归写出来理解一下就可以。head.next.nex指的是最后一个结点的下一个结点,指向的是最后的结点的前一个结点
head.next=null; //上面一行是,最后一个结点指向head结点,这一行代表的是head结点后面指向null
return temp; //最后返回新的头结点temp
}
25、《剑指offer》16 合并两个排序的链表
//方法一:递归实现
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1==null)
return list2;
if(list2==null)
return list1;
ListNode head=null; //头节点
if(list1.val<=list2.val){
head=list1;
head.next=Merge(list1.next,list2);
}else{
head=list2;
head.next=Merge(list1,list2.next);
}
return head;
}
//方法二:非递归实现
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1==null)
return list2;
if(list2==null)
return list1;
ListNode head=new ListNode(-1);//头节点
ListNode thehead=head;
while(list1!=null && list2!=null){
if(list1.val<=list2.val){
thehead.next=list1;
list1=list1.next;
}else{
thehead.next=list2;
list2=list2.next;
}
thehead=thehead.next;
}
if(list1!=null) //归并完需要检查哪个链表还有剩余
thehead.next=list1;
if(list2!=null)
thehead.next=list2;
return head.next;
}
26、《剑指offer》25 复杂链表的复制,既要复制它的下一个指针next,又要复制它的random指针
public class Solution {
public RandomListNode Clone(RandomListNode pHead){
if(pHead==null)
return null;
//(1)先按next复制,但是把复制后的节点放到对应原节点后面
CopyNodes(pHead);
//(2)依次添加random指针
addRandom(pHead);
//(3)按照奇偶位置拆成两个链表
return ReconnectNodes(pHead);
}
//先按next复制,但是把复制后的节点放到对应原节点后面
public void CopyNodes(RandomListNode pHead){
RandomListNode head=pHead;
while(head!=null){
RandomListNode temp=new RandomListNode(head.label);
temp.next=head.next; //复制一个结点,插在对应的原节点的后面
temp.random=null;
head.next=temp;
head=temp.next;
}
}
//依次添加random指针
public void addRandom(RandomListNode pHead){
RandomListNode head=pHead;
while(head!=null){
RandomListNode head_new=head.next;
if(head.random!=null)
head_new.random=head.random.next;
head=head_new.next;
}
}
//按照奇偶位置拆成两个链表
public RandomListNode ReconnectNodes(RandomListNode pHead){
if(pHead==null)
return null;
RandomListNode head=pHead;
RandomListNode pHeadClone=head.next; //pHeadClone 是我们需要返回的头指针,不能再这个基础上操作,所以下一行要用一个新的指针指向它来操作。说明:head是对pHead的复制,headClone是对pHeadClone的复制
RandomListNode headClone=pHeadClone;
while(head!=null){
head.next=headClone.next; //代入例子理解,这一行的作用即A->B
head=head.next; //把head的指针指向B
if(head!=null){
headClone.next=head.next; //A‘->B’
headClone=headClone.next; //把headClone的指针指向B'
}
}
return pHeadClone;
}
}
27、《剑指offer》36 两个链表后半部分的结点完全相同,找到两个链表的第一个公共结点
public ListNode getIntersectionNode(ListNode pHead1, ListNode pHead2) {
//方法3
if(pHead1==null || pHead2==null)
return null;
ListNode head1=pHead1,head2=pHead2;
//输入的head1和head2分别是两个不同的链表,然后进入循环
//然后比较head1和head2,一开始head1和head2不可能相等,所以得到的值是后者,即head1=head1.next,head2=head2.next,不断比较下一个
//直到两个链表都到了末端,这时候head1接上head2的链表,head2的链表,同时在while的循环条件中比较,直到两个链表的结点相等,如果不等还是head1=head1.next, / //head2=head2.next,不断比较下一个,直到相等
while(head1!=head2){
head1 = head1!=null ? head1.next:pHead2;
head2 = head2!=null ? head2.next:pHead1;
}
return head1;
}
28、《剑指offer》55 找到链表中环的入口结点
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
//链表中环的入口结点
if(pHead==null)
return null;
ListNode meetingNode=hasCycle(pHead);
if(meetingNode==null) //不存在环
return null;
ListNode head=pHead;
while(head!=meetingNode){
head=head.next;
meetingNode=meetingNode.next;
}
return head;
}
public ListNode hasCycle(ListNode pHead){
//判断是否有环,快慢指针法
if(pHead==null || pHead.next==null)
return null;
ListNode slow=pHead,fast=pHead;
while(fast!=null && fast.next!=null){
slow=slow.next;
fast=fast.next.next;
if(slow==fast)
return slow;
}
return null;
}
}
29、《剑指offer》56 删除链表中连续重复的结点
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
if(pHead == null || pHead.next == null){
return pHead;
}
ListNode head = new ListNode(0);
head.next = pHead;
ListNode pre = head;
ListNode last = head.next;
while(last != null){
if(last.next != null && last.val == last.next.val){
//找到最后一个相同点
while(last.next != null && last.val == last.next.val){
last = last.next;
}
pre.next = last.next;
last = last.next;//zy
}else{
pre = pre.next;
last = last.next;
}
}
return head.next;
}
}
/*1. 首先添加一个头节点,以方便碰到第一个,第二个节点就相同的情况
2.设置 pre ,last 指针, pre指针指向当前确定不重复的那个节点,而last指针相当于工作指针,一直往后面搜索。*/
30、《剑指offer》56 删除链表中的指定结点
public ListNode deleteNode(ListNode head, int val) {
if(head == null)return head;
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = dummy.next;
while(cur != null) {
if(cur.val == val){
pre.next = cur.next;
break;
}
pre = pre.next;
cur = cur.next;
}
return dummy.next;
}
四、树和二叉树(共15道题目)
30、《剑指offer》4 根据树的前序遍历和中序遍历结果,重建二叉树
编程实现(Java):
详细的代码还是看这里吧!!!class Solution {
int[] preorder;
HashMap<Integer,Integer> hashMap = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder; // 构建数组存储前序遍历的所有结点,方便找到根节点
for(int i=0;i<inorder.length;++i) { // 构建HashMap来存储中序遍历的所有结点,方便更快的找到根节点
hashMap.put(inorder[i],i);
}
TreeNode node = recur(0,0,inorder.length-1); // 0代表左遍历边界,inorder.length - 1代表右遍历边界
return node;
}
public TreeNode recur(int root,int left,int right) { //root是根节点的下标,left是左遍历边界的下标,right是右遍历边界的下标
if(left > right) return null; //递归终止条件
TreeNode node = new TreeNode(preorder[root]); //建立根节点
int index = hashMap.get(preorder[root]); //index是根节点在中序遍历的下标
node.left = recur(root+1,left,index-1); //递归左子树
node.right = recur(root+index-left+1,index+1,right); //递归右子树,root+index-left+1是根节点索引 + 左子树长度 + 1即为右子树的根节点
return node;
}
}
31、《剑指offer》17 判断树1是否拥有树2的子结构
public boolean isSubStructure(TreeNode A, TreeNode B) {
//只有(当A、B都非空)且((A包含B)||(A左子树包含B)||(A的右子树包含B))
if(A == null || B == null) return false;
return ( contain(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B) );//先序遍历,先判断当前根节点是否满足条件,再判断其左右子树是否满足条件
}
public boolean contain(TreeNode A,TreeNode B) {
if(B==null) {return true;} //当B为空的时候,说明查询完A了,A都包含B
if(A==null || A.val != B.val) {return false;} //当A为空的时候,说明查询完A了,A不包含B。或者A、B的值不等也不包含
return contain(A.left,B.left) && contain(A.right,B.right); //A的每一个结点都要和B每一个结点相同
}
32、《剑指offer》18 二叉树的镜像
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
TreeNode temp = root.left; //初始化节点 tmptmp ,用于暂存 rootroot 的左子节点;
root.left = mirrorTree(root.right); //开启递归 左子节点 ,并将返回值作为 rootroot 的 右子节点 。
root.right = mirrorTree(temp); //开启递归 右子节点 ,并将返回值作为 rootroot 的 左子节点 。
return root;
}
}
33、《剑指offer》22 从上往下打印二叉树
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
//思路:使用队列来实现
ArrayList<Integer> list=new ArrayList<>();
if(root==null)return list;
Queue<TreeNode> queue=new LinkedList<>(); //定义一个队列
queue.add(root);
while(queue.size()!=0){
TreeNode temp=queue.poll(); //把队头元素丢到list中
list.add(temp.val);
if(temp.left!=null) queue.add(temp.left); //左孩子不空,左孩子进队列
if(temp.right!=null) queue.add(temp.right);//右孩子不空,右孩子进队列
}
return list;
}
34、《剑指offer》23 判断输入数组是否为二叉排序树的后序遍历结果
编程实现(Java):class Solution {
public boolean verifyPostorder(int[] postorder) {
return recur(postorder, 0, postorder.length - 1);
}
public boolean recur(int[] postorder, int i, int j) {
if(i >= j) return true;
int p = i;
while(postorder[p] < postorder[j]) p++;
int m = p;
while(postorder[p] > postorder[j]) p++;
return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
}
}
35、《剑指offer》24寻找二叉树中和为某一值的路径
public class Solution {
//这个res集合用来存储符合题目条件的集合,(用集合来存储集合,所以是
36、《剑指offer》26 将二叉搜索树转换成排序的双向链表
编程实现(Java):public class Solution {
public TreeNode Convert(TreeNode pRootOfTree) {
//根据中序遍历采用递归依次实现
if(pRootOfTree==null) return null; //判空
TreeNode root=pRootOfTree; //root即存的根节点
TreeNode curEndoflink=null; //curEndoflink保存的是当前已经排好的链表的最后一个节点
Convert(root,curEndoflink); //调用函数把将二叉搜索树转换成排序的双向链表
//通过循环,找到头结点,然后返回
while(pRootOfTree!=null && pRootOfTree.left!=null){ //链表头是最左边
pRootOfTree=pRootOfTree.left;
}
return pRootOfTree; //返回头结点
}
//curEndoflink保存的是当前已经排好的链表的最后一个节点
public TreeNode Convert(TreeNode pRootOfTree,TreeNode curEndoflink){
if(pRootOfTree==null) return null;
else{TreeNode root=pRootOfTree;}
//将左子树构建为链表
if(root.left!=null)
curEndoflink=Convert(root.left,curEndoflink);
//将根接在左子树的链表之后
root.left=curEndoflink;
if(curEndoflink!=null)
curEndoflink.right=root;
curEndoflink=root; //引用改变值,需要return
//将右子树构建为链表
if(root.right!=null)
curEndoflink=Convert(root.right,curEndoflink);
return curEndoflink;
}
}
37、《剑指offer》38计算二叉树的深度
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; //为什么要加1,是因为1代表根节点的深度
}
}
38、《剑指offer》39输入一棵树,判断是否为平衡二叉树
public boolean isBalanced(TreeNode root) {
if(root == null)return true;
return Math.abs(recur(root.left) - recur(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right); //先序遍历,先判断当前根节点是否满足条件,再判断其左右子树是否满足条件
}
public int recur(TreeNode root) {
if(root==null) return 0;
return Math.max(recur(root.left),recur(root.right)) + 1;
}
39、《剑指offer》39 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。
如果一个结点没有右子树,那么需要再进一步考虑,不难知道:如果这个结点是其父结点的左结点,那么根据中序遍历规则,它的下一个结点就是它的父结点。
第三种情况略复杂一些,当一个结点既没有右子树,也不是其父结点的左结点时,我们可以沿着指向父结点的指针一直向上遍历,直到找到一个是它自身的父结点的左孩子的结点,如果这样的结点存在,那么这个结点的父结点就是我们要找的下一个结点。
举例:
以上图中的树为例,其中序遍历序列是:d,b,h,e,i,a,f,c,g。 public boolean isBalanced(TreeNode root) {
if(root == null)return true; //这里一共有两个递归,第一个递归的终止条件就是roor==null
return Math.abs(recur(root.left) - recur(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right); //这里相当于先序遍历,Math.abs相当于先序遍历里面的输出语句,而后面的就是递归左右子树。他的整个执行顺序应该就是先递归左右子树,然后再让Math.abs判断子树的深度
}
public int recur(TreeNode root) {
if(root==null) return 0; //输出长度的递归终止条件一律是return 0,输出boolean的条件一律是return null
return Math.max(recur(root.left),recur(root.right)) + 1; //+1应该是加上根节点
}
40、《剑指offer》58 判断一棵二叉树是不是对称的
public boolean isSymmetric(TreeNode root) {
if(root == null)return true; // 如果根节点为空,那肯定是对称
return recur(root.left,root.right); //好,接下来我就要判断这个根节点的左右子树是否都是对称了
}
public boolean recur(TreeNode L,TreeNode R) {
if(L == null && R == null) return true; //正常来说,这个返回true的判断条件应该是L.left == R.right && L.right == R.left,但是他要一直递归下去,因此最后的终止条件是到达叶子结点,也就是L == null,R == null
if(L == null || R == null || L.val != R.val) return false; //当左右子树有一个已经到达叶子结点没有了,已经null了,或者值不相等那就是false
return recur(L.left,R.right) && recur(L.right,R.left); //记住,树的题大多数递归,递归就要记住两个点:终止条件+递归条件
}
41、《剑指offer》59 按照之字型打印二叉树
实际上,层次遍历我们都是借助一定的数据容器来实现的,比如按行打印使用的是队列。在本题,我们使用的是栈,具体分析如下:我们可以设置两个辅助栈,在打印某一层的结点时,将下一层的子结点保存到相应的栈里;如果当前打印的是奇数层(第一层、第三层等),则先保存左子节点再保存右子结点到第一个栈中,如果当前打印的是偶数层(第二层、第四层等),则先保存右子结点再保存左子结点到第二个栈中。
举例:
编程实现(Java):import java.util.ArrayList;
import java.util.Stack;
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
/*
思路:之字形打印,用两个栈来实现
打印奇数行时,将他的左右节点保存到另一个栈中,先左后右
打印偶数行时,同样将左右节点保存到栈中,先右后左
*/
ArrayList<ArrayList<Integer>> res=new ArrayList<>(); //result用来存储结果
if(pRoot==null)return res; //判空,直接返回result结果,即为空
Stack[] stack=new Stack[2]; //stack[0]保存偶数层,stack[1]保存奇数层,注意java不支持泛型数组
stack[0]=new Stack();
stack[1]=new Stack();
TreeNode root=pRoot; //根节点
stack[1].push(root); //根节点先放在保存奇数层的栈stack[1]
int num=1; //当前打印的是第几层
while((!stack[0].isEmpty())||(!stack[1].isEmpty())){ //有一个栈不为空
int flag=num%2; //当前要打印的栈
ArrayList<Integer> row=new ArrayList<>(); //row存取每一行的结点
while(!stack[flag].isEmpty()){
TreeNode temp=(TreeNode)stack[flag].pop();
if(flag==1) { //当前是奇数行,注意栈是先进后出,所以left和right反过来
if(temp.left!=null)
stack[0].push(temp.left);
if(temp.right!=null)
stack[0].push(temp.right);
}else{ //当前是偶数行
if(temp.right!=null)
stack[1].push(temp.right);
if(temp.left!=null)
stack[1].push(temp.left);
}
row.add(temp.val); //temp是每一个结点,每一个结点先存到行row集合中,最后row集合再存入到res结果集合中
}
res.add(row); //row集合再存入到res结果集合中
num++; //遍历完一行就倒下一行
}
return res; //最后返回结果结合res
}
}
42、《剑指offer》60 把二叉树打印成多行
import java.util.*;
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
public class Solution {
ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
//思路:使用队列实现
ArrayList<ArrayList<Integer>> res=new ArrayList<>();
if(pRoot==null)
return res;
Queue<TreeNode> queue = new LinkedList<>(); //借助队列实现
TreeNode root=pRoot;
queue.add(root);
while(!queue.isEmpty()){ //队列不空
//当前队列长度代表当前这一层节点个数
int len=queue.size();
ArrayList<Integer> row=new ArrayList<>();
for(int i=0;i<len;i++){ //循环次数,也就是当前这一层节点个数
TreeNode temp=queue.poll();
if(temp.left!=null)
queue.add(temp.left);
if(temp.right!=null)
queue.add(temp.right);
row.add(temp.val);
}
res.add(row);
}
return res;
}
}
43、《剑指offer》61 序列化二叉树
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
public class Solution {
//序列化
String Serialize(TreeNode root){
if(root==null) return "#,";
String res="";
res+=root.val+","; //前序遍历,根左右
res+=Serialize(root.left);
res+=Serialize(root.right);
return res;
}
//反序列化
int start=-1;
TreeNode Deserialize(String str){
if(str==null || str.length()==0)return null;
String[] strArr=str.split(",");
return Deserialize(strArr);
}
TreeNode Deserialize(String[] strArr){
start++;
if(start>=strArr.length || strArr[start].equals("#")) return null;
TreeNode cur=new TreeNode(Integer.valueOf(strArr[start]));
cur.left=Deserialize(strArr);
cur.right=Deserialize(strArr);
return cur;
}
}
44、《剑指offer》62 查找二叉搜索树的第k小的结点
上面的树中序遍历的结果:2 3 4 5 6 7 8
编程实现(Java):/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
int index = 0;//计数器;
//必须得利用result节点 因为递归不断return 得有最终的一个变量保存最终结果
TreeNode result = null;
//思路:二叉搜索树按照中序遍历的顺序打印出来正好就是排序好的顺序。
//所以,按照中序遍历顺序找到第k个结点就是结果。
TreeNode KthNode(TreeNode pRoot, int k)
{
if(pRoot == null){
return pRoot;
}else{
KthNode(pRoot.left, k);
index++;
if(index == k){
result = pRoot;
}
KthNode(pRoot.right, k);
}
return result;
}
}
五、回溯法(共2道题目)
六、栈和队列(共3道题目)
七、递归和循环(共4道题目)(30分钟复习完这里十道题)
50、《剑指offer》7 斐波那契数列
看到这个公式,非常自然的可以想到直接用递归解决。但是这里存在一个效率问题,以求f(10)为例,需要先求出前两项f(9)和f(8),同样求f(9)的时候又需要求一次f(8),这样会导致很多重复计算,下图可以直观的看出。重复计算的结点数会随着n的增加而急剧增加,导致严重的效率问题。
编程实现(Java):class Solution {
public int fib(int n) {
int MOD = 1000000007;
if(n<2) {return n;}
int p=0,q=0,r=1;
for(int i=2;i<=n;++i) {
p = q;
q = r;
r = (p+q) % MOD;
}
return r;
}
}
51、《剑指offer》8 青蛙跳台阶
public int fibo(int n) {
if(n==1 || n==2) return n;
return fibo(n-1) + fibo(n-2)
}
52、《剑指offer》9 青蛙变态跳台阶
public class Solution {
public int JumpFloorII(int target) {
return (int)Math.pow(2, target-1);
}
}
53、《剑指offer》10 矩阵覆盖
public int fibo(int n) {
if(n<=2) {
return n;
}
return fibo(n-1)+fibo(n-2);
}
八、数学(共3道题目)
54、《剑指offer》12 数值的整数次方
这里为了简单,我们通过递归直接实现,因此也可以不求负数的绝对值,因为一个数每次除以2,最终只有可能是0,1或者-1,将其作为递归结束条件,可以直接写出以下简洁代码。
编程实现(Java):class Solution {
public double myPow(double x, int n) {
if(n==0) return 1;
if(n==1) return x;
if(n==-1) return 1/x;
double qi = myPow(x,n/2);
double ou = myPow(x,n%2);
return qi * qi * ou; //这里qi,ou写错了,不是奇数和偶数的意思,他的意思是不管你奇数还是偶数,都需要先求n/2,还得求两次,最后再求多余的指数即n%2,所以等于 qi * qi * ou(看上面为奇数的公式,当n为偶数,n%2为0,ou为1,不影响计算结果)
}
}
55、《剑指offer》47 求1+2+3+……+n
public int sumNUMs(int n) {
/*
等差数列求和:S=n(n+1)/2 无法使用
1.需利用逻辑与的短路特性实现递归终止。
2.当n==0时,(n>0)&&((sum+=sumNUMs(n-1))>0)只执行前面的判断,为false,然后直接返回0;
3.当n>0时,执行sum+=sumNUMs(n-1),实现递归计算sumNUMs(n)。
*/
boolean t = (n > 1) && (n = n + sumNums(n - 1))!= 0;
return n; //最后的返回值是n,不是1,是因为每一层返回去的是N,而不是1,只有最后一层返回去的才是1
}
56、《剑指offer》48 不用加减乘除做加法
(做的时候记得搜一下,二进制加法是怎么计算的,好理解好多)
二进制加法百度经验public class Solution {
/*
思路:用位运算代替加法
三步走:第一步:不考虑进位加 第二位:考虑进位 第三步:原结果加上进位
*/
public int Add(int num1,int num2) {
//递归实现
if(num2==0) return num1;
int sum=num1^num2; //第一步,两个加数做异或运算,只算加法部分不考虑进位
int carry=(num1&num2)<<1; //第二步,两个加数做与运算,考虑进位
return Add(sum,carry); //第三步,把上面两个结果加起来,直到不用进位为止
}
}
57、《剑指offer》二进制中1的个数
public int hammingWeight(int n) {
int index = 32;
int result = 0;
for(int i=0;i<index;i++) {
if(((n>>i) & 1) == 1) {
result ++;
}
}
return result;
}
九、其他(共10道题目)
《剑指offer》给5个数,判断是否为扑克牌中的顺子
public boolean isStraight(int[] nums) {
Set<Integer> repeat = new HashSet<>(); //hashset和ArrayList尖括号里面都是一个参数,只有hashtable尖括号里面才是两个参数
int max = 0, min = 14;
for(int num : nums) {
if(num == 0) continue; // 跳过大小王
max = Math.max(max, num); // 最大牌
min = Math.min(min, num); // 最小牌
if(repeat.contains(num)) return false; // 若有重复,提前返回 false
repeat.add(num); // 添加此牌至 Set
}
return max - min < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
}
约瑟夫问题,源泉中最后剩下的数字
public int lastRemaining(int n, int m) {
//核心思想,就是从最后一轮剩下的数字下标倒推出他在原来数组的下标
int res = 0 ;
for(int i=2;i<=n;i++) {
res = (res + m) % i;
}
return res;
}