-
剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer
-
剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer
-
目录
- Number36:两个链表的第一个公共结点
- Number37:数字在排序数组中出现的次数
- Number38:二叉树的深度
- Number39:平衡二叉树
- Number40:数组中只出现一次的数字
题目36 两个链表的第一个公共结点
题目描述:输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
分析:因为是单链表,只有一个next指针,则公共结点之后的内容相同。那么这个数据结构的拓扑形状看起来像一个Y,而不可能像X:
有几个方法:
方法1:暴力遍历(不推荐)
在第一链表上顺序遍历每个结点,每遍历到一个结点的时候,在第二个链表上顺序遍历每个结点。如果在第二个链表上有一个结点和第一个链表上的结点一样,说明两个链表在这个结点上重合,于是就找到了它们的公共结点。如果第一个链表的长度为m,第二个链表的长度为n,显然该方法的时间复杂度是O(mn)。
这种方法不推荐,代码较为简单不赘述;
方法2:利用栈存储
分别把两个链表的结点放入两个栈里,这样两个链表的尾结点就位于两个栈的栈顶,接下来比较两个栈顶的结点是否相同。如果相同,则把栈顶弹出接着比较下一个栈顶,直到找到最后一个相同的结点。
如果链表的长度分别为m和n,那么空间复杂度是O(m+n)。这种思路的时间复杂度也是O(m+n)。和最开始的蛮力法相比,时间效率得到了提高,相当于是用空间消耗换取了时间效率。
//利用栈的方法
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2)
{
if(pHead1 == null || pHead2 == null)
return null;
Stack<ListNode> nodes1 = new Stack<>();
Stack<ListNode> nodes2 = new Stack<>();
//将2个链表的结点保存到栈
while (pHead1 != null)
{
nodes1.push(pHead1);
pHead1 = pHead1.next;
}
while (pHead2 != null)
{
nodes2.push(pHead2);
pHead2 = pHead2.next;
}
pHead1 = null;
pHead2 = null;
ListNode common = null;
//当2个栈都不为null的时候,取出栈的元素进行比较
while((!nodes1.isEmpty()) && (!nodes2.isEmpty()))
{
pHead1 = nodes1.pop();
pHead2 = nodes2.pop();
if(pHead1 == pHead2)
{
common = pHead1;//将当前结点保存
}
else
{
break;
}
}
return common;
}
方法3:利用栈存储
我们可以首先遍历两个链表得到它们的长度,就能知道哪个链表比较长,以及长的链表比短的链表多几个结点。在第二次遍历的时候,在较长的链表上先走若干步,接着再同时在两个链表上遍历,找到的第一个相同的结点就是它们的第一个公共结点。
上述思路与借助栈的方法的时间复杂度都是O(m+n),但我们不再需要辅助的栈,因此提高了空间效率。
但是很奇怪,我们这些代码在编译器上面可以找到相应的结点,但是在牛客网上没办法通过。反正思路是正确的就可以!
public static ListNode FindFirstCommonNode2(ListNode pHead1, ListNode pHead2)
{
if(pHead1 == null || pHead2 == null)
return null;
int length1 = getListLength(pHead1);
int length2 = getListLength(pHead2);
ListNode longList = null;
ListNode shortList = null;
int diff = length1 - length2;
if(diff < 0)
{
longList = pHead2;
shortList = pHead1;
diff = length2 - length1;
}
else
{
longList = pHead1;
shortList = pHead2;
}
//将较长的链表后移diff长度
while (diff!=0)
{
longList = longList.next;
diff--;
}
while (longList!=null && shortList!=null && longList != shortList)
{
longList = longList.next;
shortList = shortList.next;
}
return longList;
}
private static int getListLength(ListNode node)
{
int length = 0;
ListNode tempNode = node;
while(tempNode != null)
{
length++;
tempNode = tempNode.next;
}
return length;
}
方法4:推荐
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
这种方法的时间复杂度是O(m+n),不需要消耗额外的空间。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode p1 = pHead1;
ListNode p2 = pHead2;
/**
* p1 遍历过程中,如果不为null,就指向下一个结点;当 p1 遍历完 pHead1 之后,它指向null,此时他肯定还不等于 p2,将 p1 指向pHead2链表头,
* 继续向下遍历;同样的,p2 在遍历完 pHead2之后,指向 pHead1 继续遍历,这样就会遇到 p1=p2 的情况!
* p1 与 p2 在遍历第一个链表的时候肯定不会相遇
*/
while (p1 != p2)
{
p1 = (p1==null)?pHead2:p1.next;
p2 = (p2==null)?pHead1:p2.next;
}
return p1;
}
题目37 数字在排序数组中出现的次数
题目描述:统计一个数字在排序数组中出现的次数。
思路:
1、首先,遍历数组肯定就能知道某个数字的个数,此时的时间复杂度O(n)。但是,在数据大的情况下,这种算法时间消耗过多。
2、我们注意到,任务本质上是查找问题,而且是排序好的数组,可以尝试用二分查找算法。
这样我们可以通过二分法,找到一个k(k是要查找的数字),然后根据这个k的位置,继续使用二分法,知道找到k第一次出现以及最后一次出现的位置,这样就可以很容易算得出现的次数。
当数组长度很大,而k出现的次数远小于数组长度且k分布于数组中间的时候,这种算法的效率比遍历整个数组的方法高很多。
但是如果k是n个呢?这个算法本质上时间复杂度还是O(n)。
关于此处使用的二分法的图解,参考:添加链接描述,这位老哥讲得很好!
我们可以使用循环二分查找与递归二分查找,如下代码:
public class OfferGetTest37
{
public static int GetNumberOfK(int [] array , int k) {
if(array == null || array.length == 0)
return 0;
//使用循环二分查找,找到k在数组中第一次出现的位置——从0到array.length-1 开始查找
int firstPlace = getFirstPlace(array , k , 0 , array.length-1);
//使用递归二分查找,找到k在数组中最后一次出现的位置——从0到array.length-1 开始查找
int lastPlace = getLastPlace(array , k , 0 , array.length-1);
if(firstPlace!=-1 && lastPlace!=-1)
return lastPlace-firstPlace+1;//这个就是k在数组中出现的次数
return 0;//如果firstPlace、lastPlace某一个为-1,说明查找失败,返回0
// 查找失败的原因,要么是上面的数组不对,要么是k在数组中不存在,找不到它出现的第一位和最后一位
}
//k在数组中第一次出现的位置——循环
public static int getFirstPlace(int[] array , int k , int begin , int end)
{
if(begin > end)
return -1;//说明数组长度为0,直接返回-1,查找失败(其实前面判断过,这里可以省略)
int middle = 0;
while (begin<=end)//begin=end 的时候,仍然需要进行查找,此时 mid=(begin+end)/2
{
middle = (begin+end)/2;
if(array[middle] > k)
{
end = middle-1;
}
else if(array[middle] < k)
{
begin = middle+1;
}
/*
此处,排除 array[middle] > k 与 array[middle] < k,就剩下array[middle] = k,此时需要判断middle-1位置是否为k,
但是,在这之前,循环有可能遍历到数组首位,middle-1可能不存在,因此需要讨论middle-1是否存在。
middle-1不存在,此时array[middle] = k,那么middle就是k第一次出现的位置,也是数组首位;
middle-1存在,如果 array[middle-1] != k,那么middle就是k第一次出现的位置;
middle-1存在,如果 array[middle-1] = k,说明middle不是k第一次出现的位置,需要继续向左二分查询,end=middle-1;
那么我们只要排除 middle-1 存在且 array[middle] = k ,此时继续二分查询;剩下的情况直接返回middle即可!
*/
else if(middle-1>=0 && array[middle-1] == k)
{
end = middle-1;
}
else
{
return middle;
}
}
return -1;//如果循环完没有找到,说明数组中不存在k这个数,返回-1表示 数组不存在k
}
//k在数组中最后一次出现的位置——递归,递归方法涵义:查找数组array在 begin到end范围内 k在数组中最后一次出现的位置
public static int getLastPlace(int[] array , int k , int begin , int end)
{
//1、解决规模最小问题——递归到最后 begin>end 的时候,还没有返回middle,说明查找的k在数组中不存在,返回-1(这里不能省略)
if(begin>end)
return -1;//规模最小问题(数组无法查找到k的情况)
int middle = (begin+end)/2;
//2、解决规模较小的问题——注意递归方法的涵义
if(array[middle] > k)
{
//当 array[middle] > k 的时候,我们调用 getLastPlace 查找数组array在 begin到middle-1范围内 k在数组中最后一次出现的位置(这个是规模较小的问题)
//3、将较小问题的解整合成为较大问题的解:当较小问题查找到后,直接返回查找结果,这个查找结果也是较大问题的查找结果。
return getLastPlace(array , k , begin , middle-1);
}
else if(array[middle] < k)
{
return getLastPlace(array , k , middle+1 , end);
}
else if(middle+1<array.length && array[middle+1] == k)//middle+1<array.length:middle+1存在
{
return getLastPlace(array , k , middle+1 , end);//继续递归
}
else
{
return middle;//这个也是规模最小问题(能查找到k最后一次出现位置的情况)
}
//当递归到规模最小完问题,结果就会一级一级递归返回,直到返回最大问题的结果。
}
public static void main(String[] args)
{
int arr[] = null;
System.out.println(GetNumberOfK(arr , 7));
}
}
题目38 二叉树的深度
题目描述:输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
注意该二叉树不是满二叉树、也不是完全二叉树,其最深的结点可能在左子树,也可能在右子树。
//查询以root为根的二叉树的深度
public int TreeDepth(TreeNode root)
{
//当遍历到二叉树的末尾的时候,root=null,此时这里没有结点,返回0
if(root == null)
return 0;
//由于二叉树的最深处可能在左子树,也可能在右子树,我们需要分别查询左子树以及右子树的深度,找出最深的深度
int lDepth = TreeDepth(root.left);
int rDepth = TreeDepth(root.right);
//查询到左子树与右子树的最大深度,并加上当前结点的深度1,就是以root为根的树的最大深度
return 1+ (lDepth>rDepth ? lDepth : rDepth);
}
//上面代码可以简化为如下
public int TreeDepth1(TreeNode root)
{
return root==null ? 0 : 1+Math.max(TreeDepth1(root.left) , TreeDepth1(root.right));
}
题目39 平衡二叉树
题目描述:输入一棵二叉树,判断该二叉树是否是平衡二叉树。在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树。
分析:一般情况下,平衡二叉树就是AVL树,它首先要满足二叉搜索树性质,其次其平衡性指的是:对于平衡二叉树所有的结点,这些结点左右子树高度之差不超过1。但是在本题中,没有二叉搜索树的要求,只对平衡与否进行判断即可。
方法1:我们很容易想到,在遍历树的每一个结点时,求其左右子树的深度,判断深度之差,如果每个结点的左右子树深度相差都不超过1,那么就是一棵平衡二叉树。本思路直观简洁,但是我们从树的根结点开始判断,每次都需要从当前结点递归到树的叶子结点,这样才能算出当前结点的高度,这样使得很多结点需要重复遍历多次,时间效率不高。
方法2:为了避免重复遍历,我们采用后序遍历的方式遍历二叉树的每个结点,这样在遍历到每个结点的时候就已经访问了它的左右子树。即我们先求某个结点左右孩子结点的高度,同时计算左右孩子结点的平衡性,如果左右孩子结点已经不平衡,直接返回-1,没必须再计算当前结点的高度,同样直接返回-1。这种方法从底层开始计算每一个结点的高度与其平衡性,从而使得所有的结点最多被遍历一次!
代码如下:
public boolean IsBalanced_Solution(TreeNode root)
{
int height = getHeight(root);
//在计算root结点高度的过程中,如果返回-1,说明树中有不平衡结点,树不是平衡二叉树。
if(height == -1)
return false;
//如果返回root树的高度,则该树是平衡二叉树!
return true;
}
/** 用于获取node结点高度的方法
1) 这个方法当当前结点左右孩子结点不平衡、或者当前结点不平衡,则不会返回当前结点高度,而是返回-1;
如果当前结点平衡则会返回当前结点高度。
2) 只要底层有一个结点不平衡,就会全部返回-1,那么其他上面的结点也不需要计算高度,因为已经直接返回-1了;
这个方法即在计算当前结点高度之前,先计算左右孩子结点高度,并判断左右孩子结点是否平衡
*/
private int getHeight(TreeNode node)
{
if(node == null)
return 0;//空结点高度为0
//先求当前结点左右孩子结点高度,并计算左右孩子结点平衡性(后序遍历会递归到叶子结点开始)
int leftNodeHeight = getHeight(node.left);
if(leftNodeHeight == -1)
return -1;//如果发现子结点不满足平衡性返回-1,整棵树不满足平衡性,那么当前结点的平衡性不需要计算,直接返回-1
int rightNodeHeight = getHeight(node.right);
if(rightNodeHeight == -1)
return -1;
//如果孩子结点都平衡,我们利用左右孩子结点的高度,计算当前结点的平衡因子
int balanceFactor = Math.abs(leftNodeHeight - rightNodeHeight);
if(balanceFactor > 1)
return -1;//如果当前结点不平衡,直接返回-1
else
return 1 + Math.max(rightNodeHeight , leftNodeHeight);
}
题目40 数组中只出现一次的数字
题目描述:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
方法1(推荐)
首先,对于异或运算,有如下规则:
a^a = 0;
a^0 = a;
a^b^c = a^(b^c) = (a^b)^c
当数组中只有一个数出现一次,其他数都出现2次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。
依照这个思路,对于有2个数只出现1次,其他数出现2次的数组(假设2个数是A与B)。我们首先还是先异或,剩下的数字肯定是A、B异或的结果。这个结果的二进制中的1,表现的是A和B同一位上的值不同。
//这个结果的二进制中的1,表现的是A和B同一位上的值不同
如A=101,B=110,那么A^B,可以表示为:
101
110
————
011
我们发现A与B对应位上不相等,这一位异或的结果才是1
那么我们可以根据这个特性,来把A与B区分开。我们找到A与B异或结果中第一个结果为1的位,假设是第3位,接着把原数组分成两组,分组标准是第3位是否为1。如此,相同的数肯定在一个组,因为相同数字所有位都相同,而不同的数,肯定不在一组。因此,由于A与B不同,那么他们肯定被分在2组。然后把这两个组按照最开始的思路,依次异或,剩余的两个结果就是这两个只出现一次的数字。
这个方法时间复杂度为O(n)
代码如下:
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
int length = array.length;
//如果只有2个数,不需要比较直接返回
if(length == 2)
{
num1[0] = array[0];
num2[0] = array[1];
return;
}
int res = 0;//用于存储2个不同的数(假设为A、B的异或结果)
for (int i = 0; i < length ; i++)
{
res ^= array[i];
}
//找到A、B异或结果中第一个为1的位
int bitRes = 0;//用于存储res的第几位为1,位数从0开始计算
for (int i = 0; i < 32 ; i++)//因为传入的是int类型的数,最多只有32位,那么我们最多只需遍历32次就可以找到相应的位
{
if((res&1) == 0)//如果没找到1的位,将res继续右移一位
{
res >>= 1;
}
else
{
bitRes = i;//找到第一个1的位,就将这个位数赋予bitRes
break;//找到后跳出循环
}
}
//将数组按 bitRes是否为1分为2组,并且进行异或运算
for (int i = 0; i < length ; i++)
{
if(((array[i] >> bitRes) & 1) == 1)//将数右移bitRes位,判断这个位是否为1
num1[0] ^= array[i];//由于数组num1与num2初始化进来0位置必须赋值为0,我们用他们的0位置数来进行异或,最后结果也存储在0位
else
num2[0] ^= array[i];
}
//运行时间:21ms,占用内存:9624k
}
方法2
将数值放入hashmap,键为数字,值为数组中数字出现的次数,然后遍历hashmap 找到值为1的数。
这种方法时间复杂度同样是O(n),但是空间复杂度也为O(n),占用了较多空间。
//方法2,HashMap
public void FindNumsAppearOnce1(int [] array,int num1[] , int num2[])
{
if(array==null || array.length==0){
return ;
}
HashMap<Integer, Integer> hashMap = new HashMap<>();
/*
将数字的值存储为Map的键,键对应的值为数字在数组中出现的次数。
为什么不把数字出现的次数作为键?因为键必须不同,而值可以相同,数字只出现1或者2次,没办法保存那么多数字。
因此,将数字设置为键,将1/2设置为值
*/
for (int i = 0; i < array.length; i++)
{
if(hashMap.containsKey(array[i]))
hashMap.put(array[i] , hashMap.get(array[i])+1);
else
hashMap.put(array[i] , 1);
}
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for (int i = 0; i < array.length ; i++)
{
if(hashMap.get(array[i]) == 1)
arrayList.add(array[i]);
}
//注意,找的是只出现1次的数组,不是出现2次
num1[0] = arrayList.get(0);
num2[0] = arrayList.get(1);
}