剑指offer常见面试题(思路)
1.判断两个单向链表是否相交,如果相交,求出交点
判断两个单向链表是否相交?判断两个链表的最后一个节点是否相同
求相交链表的节点:
- 暴力解决:在第一个链表上顺序遍历每一个节点,每遍历到一个节点,就在第二个链表上顺序遍历每个节点。如果在第二个链表上有一个节点与第一个链表上的节点一样,则说明两个链表在这个节点上重合,于是就找到了他们的公共节点。
- 简便解法:首先遍历两个链表得到他们的长度,就能知道哪个链表比较长,以及长的链表比短的链表多几个结点。在第二次遍历的时候,在较长的链表上先走若干步,接着同时在两个链表上遍历,找到的第一个相同的结点就是他们的第一个公共结点
参考:https://blog.csdn.net/kongmin_123/article/details/82315013
二、判断链表是否有环?如果有,则求出环入口节点
- 普通解法:首先从头节点开始,依次遍历单链表的每一个节点。每遍历到一个新节点,就从头节点重新遍历新节点之前的所有节点,用新节点ID和此节点之前所有节点ID依次作比较。如果发现新节点之前的所有节点当中存在相同节点ID,则说明该节点被遍历过两次,链表有环;
- 巧妙解法:首先创建两个指针1和2(在java里就是两个对象引用),同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。此方法也可以用一个更生动的例子来形容:在一个环形跑道上,两个运动员在同一地点起跑,一个运动员速度快,一个运动员速度慢。当两人跑了一段时间,速度快的运动员必然会从速度慢的运动员身后再次追上并超过,原因很简单,因为跑道是环形的。
链表中环的入口节点:
(1)第一步是确定一个链表中是否包含环。我们可以用两个指针来解决这个问题。定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另一个指针一次走两步。如果走得快的指针追上了走得慢的指针,那么链表就包含环;吐过走得快得快的指针走到了链表的末尾(ListNode的next指向null)都没有追上第一个指针,那么链表就不包含环。
(2)第二步是找到环中节点的数目,这一步的目的就是为寻找环的入口做铺垫。我们在上面提到判断一个链表里面是否有环时用到了一快一慢两个指针。如果两个指针相遇,则表明链表中存在环。两个指针相遇的节点一定是在环中。可以从这个节点出发,一边继续向前移动一边计数,当再次回到这个节点时,就可以得到环中节点数了。
(3)第三步是找到环的入口。我们还可以利用两个指针来解决这个问题。先定义两个指针P1和P2指向链表的头结点。如果链表中的环有n个节点,则指针P1先在链表上向前移动n步,然后两个指针以相同的速度向前移动。当第二个指针指向环的入口节点时,第一个指针已经围绕着环走了一圈,又回到了入口节点。
参考:https://blog.csdn.net/kongmin_123/article/details/82313198
三、跳台阶问题
题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
分析:
本质上还是斐波那契数列,所以迭代也可以求
当成 dp 问题来想的话:首先分析问题,它最终解是由前面的解累积起来的解,如何缩小问题的规模?
首先可知,第一阶有只能一步,一种;第二阶可以两次一步、一次两步两种
若楼梯阶级 n = 3
- 跳 2 步到 3:剩下的是第一步没跳,起始跳到第一步只有一种
- 跳 1 步到 3:剩下的是第二步没跳,起始跳到第二步有两种
通过分类讨论,问题规模就减少了:
若楼梯阶级 n = n
- 跳 2 步到 n:剩下的是第 n - 2 步没跳,起始跳到第 n - 2 步设它为 pre2 种
- 跳 1 步到 n:剩下的是第 n - 1 步没跳,起始跳到第 n - 1 步设它为 pre1 种
同时可以发现第 n 阶的解法,只要用到 n - 1 和 n - 2 阶是多少,其他的不用考虑,因此用两个变量临时存下来即可
所以可以得出:
dp(i) = dp(i-2) + dp(i-1)
代码:
int JumpFloor(int number)
{
if (number==1 || number==2)
return 1;
else
return JumpFloor(number-1)+JumpFloor(number-2);
}
参考:https://www.nowcoder.com/questionTerminal/8c82a5b80378478f9484d87d1c5f12a4?answerType=1&f=discussion
四、两个栈实现一个队列
基础:
- 队列:先进先出,即插入数据在队尾进行,删除数据在队头进行;
- 栈:后进先出,即插入与删除数据均在栈顶进行。
思路:
两个栈实现一个队列的思想:用pushStack栈作为push数据的栈,用popStack栈作为pop数据的栈。
- 只要是对队列进行push操作,就将数据push入pushStack栈中。
- 要实现队列的pop操作,有二点原则,如果popStack为空的话那么我们就将pushStack所有的元素放到popStack中,然后取popStack栈顶元素就是队列的队头;如果popStack不为空的话,我们就直接获取popStack的栈顶元素。
- 对于top操作来说和pop操作类似,只是最后一步不用pop了。
参考:https://www.jb51.net/article/161528.htm
五、合并两个排序的链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
-
方法一:将两个链表结点重头开始挨个进行比较,每次选择一个值最小的节点插入到一个新表中。
-
方法二:把list2往list1的中插。 比较list2与list1的值:
-
当list2值小等于list1值时往list1的前面插,并让list2指向下一个元素 否则不进行插入,list1指向下一个结点。
-
重复上述操作,直到有一个链表为空
-
判断是哪个链表空了,如果是list2则说明list2已全部插入直接返回头结点即可。如果是list1,则将剩下的list2结点直接连到list1尾部,返回头结点即可。
-
六、求数组中出现次数超过一半的数字
- 方法1:先排序,取中间的数,若这个数在数组中出现超过长度一半,则存在;否则不存在
- 方法2:用map键值来记录每个元素出现的次数,如果该元素出现次数超过一半,返回这个数:
int MoreThanHalfNum_Solution(vector<int> numbers) {
map<int, int> m;
for (int i = 0; i < numbers.size(); ++i) {
m[numbers[i]]+=1;//相同的数每次加1
if(m[numbers[i]]>numbers.size()/2)
return numbers[i];
}
return 0;
}
参考:https://blog.csdn.net/zjwreal/article/details/88607992
七、反转链表
题目描述
输入一个链表,反转链表后,输出新链表的表头。
- 方法1:新建链表,头节点插入法。新建一个头结点,遍历原链表,把每个节点用头结点插入到新建链表中。最后,新建的链表就是反转后的链表(或者不用新建链表,把头结点的下一个节点头插法插到头结点前面)。
- 方法2:从表头遍历到表尾,每次改变当前节点和下一个节点的指向。(注意每次头结点的变化改变