-
剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer
-
目录
- Number26:二叉搜索树与双向链表
- Number27:字符串的排列
- Number28:数组中出现次数超过一半的数字
- Number29:最小的k个数
- Number30:连续子数组的最大和
题目26 二叉搜索树与双向链表
题目描述:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
分析:
1)要求双向链表排序,我们利用二叉搜索树的特性,中序遍历二叉搜索树,就可以得到排序的结点。
2)我们从二叉搜索树取出一个结点构建链表,设置该结点的左指针指向前一个结点,右指针指向下一个结点,其实我们在将取出的结点连接到双向链表的时候,只需要考虑前一个结点的右指针指向当前新的结点、当前新结点的指针指向前一个结点即可。
至于当前新结点的右指针不需要管,如果有下一个结点插入双向链表,那么当前新结点的指针再设置指向下一个结点;如果没有下一个插入,当前结点的右指针则什么都不指向(null),此时已经是原来二叉搜索树中序遍历的最后一个结点。
其实对于第一个结点和最后一个结点,他们在二叉搜索树中左右指针本来都指向null,因此他们有一边不指向任何结点并不会影响链表结构!
有疑问就在大脑里面想象一下整个过程即可!
代码如下:
private TreeNode head;//创建一个头结点,用于返回双向链表头结点
//由于插入一个链表,我们只需要考虑当前结点左指针 和 前一个结点右指针,那么我们只需要创建一个代表前一个结点的pre即可
private TreeNode pre;
public TreeNode Convert(TreeNode pRootOfTree)
{
//使用中序遍历方式找出二叉搜索树所有结点,并一个个连接到双向链表上
inOrder(pRootOfTree);
return head;//返回双向链表头结点
}
private void inOrder(TreeNode node)
{
if(node == null)
return;//如果二叉搜索树结点遍历完,不需要再向双向链表添加结点,直接结束递归
//中序遍历左子树
inOrder(node.left);
/*
对于遍历到的当前结点,我们先判断前一个结点pre是否存在,
如果不存在,说明在双向链表中,当前结点是第一个结点,将当前结点赋予pre;
如果存在,说明在双向链表中,当前结点前面有结点,将pre.right指向当前结点node,将node.left指向pre,这样当前结点就连接到双向链表,
此时,对于下一个插入的结点当前结点就是前一个结点,因此将pre指向node,便于插入下一个结点。
另外,如果pre=null,说明node是双向链表第一个结点,将其赋予head
*/
if(pre != null)
{
pre.right = node;
node.left = pre;
pre = node;
}
else
{
pre = node;
head = node;
}
//中序遍历右子树
inOrder(node.right);
}
题目27 字符串的排列
题目描述:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输入描述:输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
- 注:这里参考文章添加链接描述
分析:我们可以把整个流程分为2步:
1)求所有可能出现在第一个位置的字符,即把第一个字符与后面的字符依次交换。(for循环)
2)将某一个字符固定,求后面所有字符的排列。
而求后面所有字符的排列,我们仍然可以将其分解为上面2步。这是一个递归的过程,直到字符串的结尾,我们停止递归。
如下图:
注意点:
1)在每个分支进行完后,要将交换过的元素复位,从而不会影响其他分支;
2)题目指出可能有字符重复,所以分支末尾可能有重复的序列,在加入ArrayList要进行去重判断,不重复再加入。
代码如下:
public class OfferGetTest27
{
//将返回结果的ArrayList定义在外面,避免下面的函数传递太多的参数
ArrayList<String> result = new ArrayList<>();
public ArrayList<String> Permutation(String str)
{
if(str==null || str.length()==0)
return result;
//注意,这里返回result这个ArrayList,不要返回null。因为当 str=""的时候,返回ArrayList,里面也是"",而不是null!
Permutation(str.toCharArray() , 0);//index从0开始
Collections.sort(result);//最后,将ArrayList的字符串排列一下,因为测试用例的字符串是有顺序的,不排列最后无法通过!
return result;
}
//从strArr字符串数组的index位置开始置换字符
private void Permutation(char[] strArr , int index)
{
/**
当 index=strArr.length,说明递归到字符数组的结尾,需要将这个排列的字符数组转换为字符串,
当然,由于题目给定的字符串里面可能有重复的字符,我们这里获取字符有可能与前面获取到的字符重复,
因此在放入 result 之前需要先判断 result 里面是否存在这个字符串。
*/
if(index == strArr.length-1)//遍历到最后一个字符,不需要再替换后面的字符(也不需要替换自己)
{
String str = String.valueOf(strArr);//将这个顺序的字符数组变为字符串
if(!result.contains(str))
result.add(str);//字符串不存在才将其加入result
}
else
{
/**
如果没有递归到字符数组的末尾,我们将index位置的字符逐个与后面的字符替换,
然后将当前index位置的字符固定,通过 Permutation() 方法,递归寻找index+1位置开始的所有字符的可能排列,
直到递归到字符数组的末尾。
注意:
1)这里需要从index位置开始替换,即index位置的字符保持不变,因为这也是一种情况;
2)当index+1位置递归返回后,我们需要将原来的替换复原,如果不一层一层递归在替换后都复原,
递归后的字符数组就是乱序的,从而影响其他分支的替换。(这个过程参考解析的图)
*/
for (int i = index; i <= strArr.length-1 ; i++)
{
//index位置的字符在循环中逐个替换 i=index,index+1,...,strArr.length-1位置的字符
swap(strArr , index , i);
//替换一个字符后,将当前index位置的字符固定,从index+1位置开始递归替换
Permutation(strArr , index+1);
/**
为了不影响其他分支,我们在递归搜索完 index+1 开始位置所有可能的字符排列情况后,将替换还原,
而递归中也会将替换还原,那么到这里,就变成没有替换 index与i 位置字符之前的情况,
那么我们下面就可以将 index与i+1 进行替换,这样就可以找到index位置所有可能的字符(想象一下这个过程!)
*/
swap(strArr , index , i);
//另外,这里不需要考虑重复,因为递归到最后,重复的字符串会被剔除
}
}
}
//替换方法
private void swap(char[] arr , int p1 , int p2)
{
char temp = arr[p1];
arr[p1] = arr[p2];
arr[p2] = temp;
}
public static void main(String[] args)
{
ArrayList<String> ab = new OfferGetTest27().Permutation("ab");
for (String s : ab)
{
System.out.println(s);
}
}
}
题目28 数组中出现次数超过一半的数字
题目描述:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。
使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素相等时,令 cnt++,否则令 cnt–。如果前面查找了i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于i / 2 的话 cnt 就一定不会为 0 。
此时我们用于统计的元素,有可能是majority,也有可能不是majority,但是剩下的元素中,肯定有majority,且剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找剩下的元素,就能找出 majority。
当然还可以使用复杂度为O(n^2)的暴力法,不赘述!
public int MoreThanHalfNum_Solution(int [] array)
{
//我们使用数组第一个元素作为比较元素
int comNum = array[0];
/**
* 使用count来计算当前比较元素的个数,初始值为1个,设数组长度是n
* 1)当count=0的时候,说明前i+1个数组元素中(数组元素从0开始),比较元素的个数等于其他元素的个数。此时比较元素可能是majority,也有可能不是majority。
* 如果比较元素是 majority,由于 majority 大于n/2,那么后面的 n-i-1 个元素中,majority的数量肯定大于一半;
* 如果比较元素不是 majority,其他元素(包括majority)个数小于此时的比较元素,那么后面的 n-i-1 个元素中,majority的数量肯定大于一半;
* 因此我们可以将比较元素进行替换:comNum = array[i]; ,继续从 i+1 开始,进行下面的遍历。
* 遍历到数组最后,一定会有count一直大于0的情况,此时的比较元素就是majority。
*
* 2)替换的时候,为什么不把比较元素替换为:array[i+1],不是说从第 i+1 遍历比较吗?
* 其实这里可以替换为 comNum = array[i+1],但是我们下一次循环又会遇到 array[i+1],再次比较就出错。(其实可以设置)
* 因此我们此时将比较元素设置为:array[i],
* 其实我这里考虑的是,会不会因为从 i 开始而不是从 i+1 开始,导致从i到n-1的元素中majority个数小于一半。
* 其实不会,因为如果array[i]是majority,那么从i到n-1的元素中,majority的个数肯定大于一半;
* 如果array[i] 不是majority,很快它遇到与他不同的元素(遇到的有可能是majority,也有可能不是majority),
* 遇到的元素会使得比较元素再次替换,那么剩下的元素中majority的数量肯定大于一半。
* 例如:
* i+1 与 i 不同,那么此时比较元素替换为 array[i+1],那么从i+1到n-1的元素中,majority的个数肯定大于一半;
* 如果 i+1 与 i 相同,很快就会大量不同的其他元素,那么剩下的元素中majority的个数还是大于一半!!!
*/
for (int i = 1,count = 1; i < array.length ; i++)
{
if(array[i] == comNum)
count++;
else
count--;
if(count==0)
{
//将array[i]设置为比较元素,注意设置count=1,即此时比较元素个数为1
comNum = array[i];
count = 1;
}
}
/*
结束循环的时候,我们会获取到最后的comNum,此时这个comNum的count>=1,但是我们无法提供count的值进行判断;
此时如果原来的数组中存在majority,那么comNum在数组中的数量肯定大于 n/2,且comNum就是majority;
如果如果原来的数组中不存在majority,那么comNum在数组中的数量肯定不于 n/2;
我们计算 comNum在数组中的数目,进行比较即可。
*/
int num = 0;
for (int i : array) {
if(i == comNum)
num++;
}
return num>array.length/2 ? comNum : 0;
}
题目28 数组中出现次数超过一半的数字
题目描述:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
使用快排、计数排序、最大堆优先队列实现,各个代码如下:
最大堆,时间复杂度是:O(nlogn)
public static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k)
{
ArrayList<Integer> res = new ArrayList<>();
//注意对特殊情况进行判断。
if(input == null || input.length == 0 || k > input.length || k <= 0)
return res;
//记住java实现最大堆用最小堆加上Lambda表达式:(o1, o2) -> o2-o1 即可
Queue<Integer> queue = new PriorityQueue<>((o1, o2) -> o2-o1 );
//将数组中的元素放入堆中(注意只放k个元素)
for (int i = 0; i < input.length; i++)
{
/**
特别注意,这里 queue.size() < k,而不是queue.size() <= k。
在添加第k个之前,queue.size()=k-1<k,进入循环,添加第k个,随后 queue.size() =k跳出循环,刚刚好添加k个。
如果 queue.size() <= k,在添加k个后还满足循环,会添加第k+1个!
*/
if(queue.size() < k)
{
queue.add(input[i]);
}
else
{
if(input[i] < queue.peek())
{
//将队首(堆顶)元素移除,将更小的元素加入堆
queue.remove();
queue.add(input[i]);
}
}
}
快排,时间复杂度:O(nlogn)
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k)
{
ArrayList<Integer> res = new ArrayList<>();
if(input == null || input.length==0 || k>input.length || k<=0)
return res;
quickSort(input , 0 ,input.length-1);
for (int i = 0; i < k ; i++)
{
res.add(input[i]);
}
return res;
}
private void quickSort(int[] arr , int left , int right)
{
//当 left>= right 的时候,结束递归
if(left >= right)
return;
if(left < right)
{
//找去区间 left到right的中轴元素下标mid,并将小于中轴元素的元素放在mid左边,大于他的放在mid右边
int mid = partition(arr , left , right);
quickSort(arr , left , mid-1);
quickSort(arr , mid+1 , right);
}
}
private int partition(int[] arr , int left , int right)
{
int pivot = arr[left];//随机取最左边的元素为中轴元素
int start = left+1;
int end = right;
while (true)
{
//找到左区间第一个大于 pivot 的元素
while (start<=end && arr[start]<=pivot)
start++;
//找到右区间第一个小于于 pivot 的元素
while (start<=end && arr[end]>pivot)
end--;
//若start>end ,说明中轴元素左右两边元素摆放完毕,结束循环
if(start>end)
break;
//如果没有跳出循环,则交换start与end的元素
swap(arr , start ,end);
}
//跳出循环后,此时end为中轴元素应该放的位置,将left与end互换元素,并放回中轴元素下标:end
swap(arr , left ,end);
return end;
}
private void swap(int[] arr , int i , int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
计数排序,时间复杂度:O(n+k),这会多花费:O(k)的空间,其中k是新数组的大小。
ArrayList<Integer> res = new ArrayList<>();
if (input == null || input.length == 0 || k > input.length || k <= 0) return res;
//1、先找出input最大值最小值
int min = input[0];
int max = input[0];
for (int i = 1; i < input.length ; i++)
{
if(input[i] < min)
min = input[i];
if(input[i] > max)
max = input[i];
}
//2、随后,求出临时数组的大小,并构建临时数组
int length = max-min+1;
int[] temp = new int[length];
//3、遍历input数组,将input数组元素作为temp数组下标,个数为特莫数组下标元素对应的值
for (int i = 0; i < input.length ; i++)
{
//注意,藤牌数组区间从 0到max-min,而input数组元素从 min到max,因此应该减去min
temp[input[i]-min]++;
}
int index = 0;
//将temp元素取出放入input
for (int i = 0; i < temp.length ; i++)
{
while (temp[i]>0)
{
input[index++] = i + min;//注意将min加回去
temp[i] --;
}
}
for (int i = 0; i < k ; i++)
{
res.add(input[i]);
}
return res;
题目30 连续子数组的最大和
题目描述:HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
方法1:暴力法
如下,暴力遍历所有序列并对比,时间复杂度是O(n^2)
public static int FindGreatestSumOfSubArray1(int[] array) {
//maxSum不能赋值为0,有可能整个数组都是负数
//maxSum赋值为最开始的子序列array[0],如果没有找到比他更大的子序列,array[0]就是最大的子序列,否则就赋值为其他更大的子序列
int maxSum = array[0];
//外循环用于调整从哪个下标开始,内循环用于记录以这个下标为开始的所有子序列的和
for (int i = 0; i < array.length ; i++)
{
//thisSUm 循环记录所有以j下标为开头的子序列的值,当找到某一个子序列 thisSum>maxSum 的时候,我们就将这个子序列的值赋予maxSum
//由于下一个内循环开始之前,开始下标变换,因此thisSum必须重新赋值为0
int thisSum = 0;
for (int j = i; j < array.length ; j++)
{
thisSum += array[j];
if(thisSum > maxSum)
maxSum = thisSum;
}
}
return maxSum;
//运行时间:12ms,占用内存:9412k
}
方法2:动态规划(推荐)
先上动态规划的公式:
max(dp[i]) = getMax( max(dp[i-1]) + arr[i] , arr[i] )
首先,dp[i] 就是以数组下标为 i 的数做为结尾的最大子序列和,注意是以 i 为结尾。
比如说现在有一个数组 arr = {6,-3,-2,7,-15,1,2,2},我们使用一个 maxSum 来记录当前最大的子序列。
dp[0] 就是以下标0为结尾的子序列的最大和,显然只有一个,我们记为 dp[0] = arr[0] = 6,此时maxSum = dp[0]。
dp[1] 就是以下标1为结尾的子序列的最大和,那么 dp[1] = Max{dp[0]+arr[1] , arr[1]},既如果 dp[0]<0 ,dp[1]=arr[1],否则dp[1]=dp[0]+arr[1]。
我们求出以下标1位结尾的子序列的最大和后,判断dp[1]与maxSum的大小,如果dp[1]>maxSum,我们将maxSum=dp[1]。
....
dp[n] 就是以下标n为结尾的子序列的最大和,那么 dp[n] = Max{dp[n-1]+arr[n] , arr[n]},既如果 dp[n-1]<0 ,dp[n]=arr[n],否则dp[n]=dp[n-1]+arr[n]。
我们求出以下标n位结尾的子序列的最大和后,判断dp[n]与maxSum的大小,如果dp[n]>maxSum,我们将maxSum=dp[n]。
.....
结论:
1)我们只需要遍历数组,找到数组所有下标对应的以该下标为结尾的子序列的最大和,这就相当于判断完了数组中所有的子序列。并且,我们使用maxSum保存了这些子序列的最大值。
2)对于 dp[n]:以下标n为结尾的子序列的最大和,我们必须先找到dp[n-1]:以下标n-1为结尾的子序列的最大和,dp[n-1]已经将前面的所有情况覆盖并找到最大和,此时dp[n]的最大值只有两种情况:dp[n-1]+arr[n] 或者 arr[n]。
动态规划法的时间复杂度是O(n)。
实现代码如下:
public static int FindGreatestSumOfSubArray2(int[] array) {
//注意,maxSum目前也应该记录为array[0],不能是0,因为可能整个数组值都小于0!
int maxSum = array[0];//用于记录最大子序列和.
int thisSum = array[0];//用于记录 以数组当前下标为结尾的最大子序列和,我们将其初始化赋值为dp[0]
//从dp[1]开始查找
for (int i = 1; i <array.length ; i++)
{
if(thisSum < 0)//如果dp[n-1]<0
thisSum = array[i];//更新dp,此时dp[n]=array[n]
else//如果dp[n-1]>0
thisSum += array[i];//更新dp,此时dp[n]=array[n]+dp[n-1]
//更新maxSUm值,最后才能得到数组中的最大子序列
if(maxSum<thisSum)
maxSum = thisSum;//当当前下标的dp[n]比 以之前下标结尾的最大子序列要大,那么就将maxSum更新为dp[n]
}
return maxSum;
//运行时间:14ms,占用内存:9396k
}
还有一种递归分治的方法,参考文章:添加链接描述
当然这篇文章的方法是对的,但是里面有的步骤写错,可以参考我在文章评论指出的错误!