您的位置:首页 > 其它

有关链表的常见题型

2016-07-17 18:58 344 查看

一、线性表基本概念

1.1 顺序存储—顺序表:

线性表的顺序存储又称为顺序表,用一组地址连续的存储单元,一次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素物理位置也相邻。

优点:可以进行随机访问,只要知道下标索引即可在O(1)时间内找到指定元素。

缺点:插入和删除需要移动大量元素。

基本操作:插入、删除、查找(二分查找)、排序(归并排序)下一篇重点讲关于数组的常见题型

1.2 链式存储—单链表、双链表、循环链表

单链表:每个链表节点除了存放自身信息之外,还携带一个指向后继的指针。只能从头节点开始依次遍历,访问后继节点的时间复杂度为O(1),访问前驱节点的时间复杂度为O(n)

双链表:具有两个指针prior和next,分别指向前驱节点和后继节点。

循环链表:与单链表的区别在于表尾的指针不是null而是指向头节点,从而使整个链表形成一个环。

基本操作:头插、尾插、逆置法、双指针法、归并法

 1.3 顺序表和链表对比

(1)对于按值查找,当顺序表无序时,两者的时间复杂度都为O(n),当顺序表有序时,可采用折半查找,时间复杂度为O(logn)
(2)对于按序号查找,顺序表可以随机访问,时间复杂度为O(1),而链表的时间复杂度为O(n),顺序表的插入、删除操作,需要移动大量元素,链表的插入和删除则修改相关的节点指针即可。由于链表每个结点都带有指针域,因而存储空间比顺序存储要付出较大的代价,存储密度不够大。

二、常考试题列表(剑指Offer)

1.求单链表中节点的个数
2.从尾到头打印单链表(栈或递归)
3.查找单链表中的倒数第k个节点(参数k取值,双指针法)
4. 查找单链表的中间节点(双指针法:last走一步,first走两步,当first到达尾节点时,last的位置就是中间结点)
5. 判断一个单链表是否有环(双指针法:时间复杂度O(n))
6. 将单链表反转(注意会发生断裂)
7. 已知两个单链表head1和head2各自有序,把他们合并为依然有序的单链表(归并法)
8. 判断两个单链表是否相交(相交点后面的元素都相等):如果相交的话肯定最后一个节点相同,若不同则肯定未相交
9. 求两个单链表相交的第一个节点:(先分别求两个链表的长度,走到共同长度的位置,然后找第一个相交的节点)
10. 已知一个单链表中存在环,求进入环中的第一个节点(判断是否存在环,是则返回环内一节点,然后分成两个链表,求相交的第一个节点)
11. 以O(1)的时间复杂度删除单链表中某指定节点:覆盖法。将被删除节点的后继的节点值赋给被删除节点,连接到后继节点的下一节点。复杂度为O(1)
12.圆圈中最后剩下的数字(约瑟夫环-环形链表问题)剑指Offer面试题45:0,1...n-1这n个数围成一个圈,从数字0开始每次从这个圆圈中删除第m个数字,求出圆圈中最后剩下的数字。
比如0,1,2,3,4这5个数字围成一个圈,每次删除第3个数字,则依次删除2,0,4,1,则最后剩的是3.

三、参考代码

注意事项:

(1)首先应判断链表头指针是否为NULL,是否只有一个结点

(2)一般是用 LinkedNode curNode = head; curNode = curNode.next。

1. 求单链表中节点的个数
考察遍历链表节点。时间复杂度为O(n)

public int getLinkNumber(Node headNode){
if(headNode==null){
return 0;
}
Node curNode = headNode;
int number = 1;
while(curNode.getNextNode()!=null){
number++;
curNode = curNode.getNextNode();
}
return number;
}


2. 从尾到头打印链表
思路1:链表只有从头向尾进行遍历,如果反过来打印,则可以利用栈 后进先出的原则。
(1)先将链表从头到尾遍历保存到栈中,
(2)从栈中依次取出
注:如果链表过大,很可能超时,LeetCode中用栈的方法会提示超时,只能用递归
思路2:递归解法
先打印下一个节点,递归结束的条件:到达尾节点 node.next == null。
注:递归占用的存储空间较大,很可能当链表长时,函数调用层级太深,出现函数调用栈溢出。

//打印链表,栈方式
public void printLinkByStack(Node node){
Stack<String> stack = new Stack<String>();

while(node!=null){
//放入栈中
stack.push(node.getValue()+"");
node = node.getNextNode();
}

//打印栈
System.out.println("栈大小:"+stack.size());
while(!stack.isEmpty()){
System.out.println("倒序:"+stack.peek());
stack.pop();
}
}

//递归形式.其实是把循环转换为     (判断语句+递归)
public void printLink(Node node){
if(node.getNextNode()!=null){
printLink(node.getNextNode());
}
System.out.println(node.getValue());
}


3. 查找单链表中的倒数第k个节点(参数k取值,双指针法)
思路:
此题要注意参数k的取值,特殊情况有如下几种:
1.链表头指针为空  
2.链表总结点数小于k 
3.k=0时,求链表第0个节点。因为规定链表从倒数第一个开始算起。

尽可能用O(n)的时间,即只遍历一次链表,则定义两个指针
(1)第一个指针从链表的头指针开始遍历向前走k-1步,第二个指针不动
(2)从k步开始,两个指针同时向前移动。
(3)由于两个指针相距k-1,当第一个指针走到尾节点时,第二个指针指向的即为倒数第k个节点。

public Node getPositionKValue(Node headNode,int k){
if(headNode == null || k==0){
return null;
}
Node front = headNode;
Node behind = headNode;
//front先走k-1步
for(int i = 0;i<k-1;i++){
if(front.getNextNode()!=null){
front = front.getNextNode();
}else {
return null;
}
}
//behind开始走,直到front走到尾部
while(front.getNextNode()!=null){
behind = behind.getNextNode();
front = front.getNextNode();
}
return behind;
}


4. 查找单链表的中间节点
思路:双指针法:定义两个指针last、first,同时从头指针出发。last走一步,first走两步,当first到达尾节点时,last的位置就是中间结点)

public Node getMiddleNode(Node headNode){
if(headNode==null){
return null;
}
Node first = headNode;
Node last = headNode;
while(first.getNextNode()!=null){
if(first.getNextNode().getNextNode()!=null){
first = first.getNextNode().getNextNode();
}else{
first = first.getNextNode();
}
last = last.getNextNode();
}
return last;
}


5. 判断一个单链表是否有环(双指针法:时间复杂度O(n))

思路:双指针法:定义两个指针last、first,同时从头指针出发。last走一步,first走两步,当last和first指针相遇时,即说明链表有环,如果first链表走到了链表的末尾仍未追上走的慢的last,则说明链表不是环形链表。代码如题4。
6. 将单链表反转
思路:从头开始遍历链表,将节点摘下插入到新链表前端。

//链表反转
public Node invertLink(Node headNode){
//如果链表头指针为NULL或是只有一个节点则无需反转返回头指针即可
if(headNode==null||headNode.getNextNode()==null){
return headNode;
}
Node curNode = headNode;
Node invertheadNode = null;
while(curNode!=null){
Node preNode = curNode;
curNode = curNode.getNextNode();
preNode.setNextNode(invertheadNode);
invertheadNode = preNode;
}
return invertheadNode;
}


注意:在反转过程中容易出现链表断裂。
思路2:调整某节点之前,先保存节点的后继节点,防止发生断裂

public Node reversLink(Node headNode){
Node lastNode = null;

Node preNode = null;
Node curNode = headNode;
Node postNode = null;
if(headNode==null||headNode.getNextNode==null){
return headNode;//链表为空或是只有一个节点时,无需反转
}
while(curNode!=null){
if(curNode.getNextNode()==null){
lastNode = curNode;
}//如果else。则发生断裂。尾节点未反转过来,所以一定要小心.不管是否为尾节点,都要执行连接操作
postNode = curNode.getNextNode();
curNode.setNextNode(preNode);
preNode = curNode;
curNode = postNode;
}

return lastNode;
}


7. 已知两个单链表head1和head2各自有序,把他们合并为依然有序的单链表(归并法)
思路:归并法,类似—数组的归并排序。下面是递归和非递归实现的代码

//递归实现
public Node MergeLink(Node A_headNode,Node B_headNode){
if(A_headNode==null){
System.out.println("---a empty--");
return B_headNode;
}
else if(B_headNode == null){
System.out.println("---b empty--");
return A_headNode;
}
Node newHeadNode = null;
if(A_headNode.getKey()<B_headNode.getKey()){
newHeadNode = A_headNode;
newHeadNode.setNextNode(MergeLink(A_headNode.getNextNode(), B_headNode));
}else {
newHeadNode = B_headNode;
newHeadNode.setNextNode(MergeLink(A_headNode, B_headNode.getNextNode()));
}
return newHeadNode;
}

//非递归(棒棒哒)
public Node createMergeLink(Node headNode1,Node headNode2){
if(headNode1 == null){
return headNode2;
}else if(headNode2 == null){
return headNode1;
}
Node newHeadNode = null;
Node curNode = newHeadNode;
while(headNode1.getNextNode()!=null&&headNode2.getNextNode()!=null){
if(headNode1.getKey()<headNode2.getKey()){
if(newHeadNode==null){
newHeadNode = headNode1;
curNode = newHeadNode;
}
else {
curNode.setNextNode(headNode1);
curNode = curNode.getNextNode();
}
headNode1 = headNode1.getNextNode();
}else {
if(newHeadNode==null){
newHeadNode = headNode2;
curNode = newHeadNode;
}
else {
curNode.setNextNode(headNode2);
curNode = curNode.getNextNode();
}
headNode2 = headNode2.getNextNode();
}
}
while(headNode1!=null){
curNode.setNextNode(headNode1);
curNode = curNode.getNextNode();
headNode1 = headNode1.getNextNode();
}
while(headNode2!=null){
curNode.setNextNode(headNode2);
curNode = curNode.getNextNode();
headNode2 = headNode2.getNextNode();
}
return newHeadNode;
}


8. 判断两个单链表是否相交(相交点后面的元素都相等)
思路:如果相交的话肯定最后一个节点相同,若不同则肯定未相交。
9. 求两个单链表的第一个公共节点
公共结点的意思是-其后面的所有结点都相同,即链表是Y形状的。
思路1:顺序扫描第一个链表时,判断第二链表中是否有出现公共结点。若链表1长度为m,链表2长度为n,则时间复杂度为O(mn)
思路2:因为从公共结点开始,其后面的所有节点都相等,Y形状。那如果能从两个链表的最后一位开始扫描,查找最后一个相同的节点,即为第一个公共结点。但单向链表只能从头到尾next,若是从尾开始,则需要先把两链表放入栈中,则空间复杂度O(n+m),时间复杂度O(n+m)
思路3:因为俩个链表成Y型,若两个链表长度不等m,n,则需长的链表m先走m-n步,然后两个链表再一起扫描,直到发现结点相等的,即是要求的公共结点。时间复杂度为O(n+m),但不需借助辅助空间,方案较好

boolean isInputInvaild = false;//标记是否为非法输入

public Node findFirstSameNode(Node headNode1,Node headNode2){
Node sameNode = null;
if(headNode1==null||headNode2==null){
isInputInvaild = true;
return sameNode;
}
int length1 = getLinkLength(headNode1);
int length2 = getLinkLength(headNode2);
int diffLength = length1-length2;
Node longLink = headNode1;
Node shortLink = headNode2;
if(length1<length2){
diffLength = length2-length1;
longLink = headNode2;
shortLink = headNode1;
}
//长链表先走diffLength距离
for(int i=0;i<diffLength;i++){
longLink = longLink.getNextNode();
}
//然后共同走,直到找到相等
while(longLink!=null&&shortLink!=null&&longLink.getKey()!=shortLink.getKey()){
longLink = longLink.getNextNode();
shortLink = shortLink.getNextNode();
}
if(longLink!=null&&shortLink!=null){
sameNode = longLink;
}else {
return sameNode;
}

return sameNode;
}

public int getLinkLength(Node headNode){
int length = 0;
if(headNode==null){
return length;
}
Node curNode = headNode;
while(curNode!=null){
length++;
curNode = curNode.getNextNode();
}
return length;
}


10. 已知一个单链表中存在环,求进入环中的第一个节点
思路1:首先判断是否存在环,若不存在结束。在环中的一个节点处断开(当然函数结束时不能破坏原链表),这样就形成了两个相交的单链表,求进入环中的第一个节点也就转换成了求两个单链表相交的第一个节点.
思路2:
(1)先判断是否有环,是的话得到环内某节点,即相遇节点(两个指针first,last,first走两步,last走一步,如果first和last相遇则说明存在环,返回相遇节点)
(2)得到环内节点个数。(设置指针从meetingNode出发,统计经过多少步,会再次到达meetingNode时,即为节点的个数count)
(3)设置两个指针p、q,让p先前进count个,然后p和q以相同的速度向前移动,直到它们相遇,相遇的节点就是环的入口节点。
思路1代码:

public Node getRingLinkNode(Node headNode){
//当链表头节点为空 或 只有头节点时,返回null
if(headNode==null||headNode.getNextNode()==null){
return null;
}
//首先判断是否有环,有的话找到环中的一个节点
Node meetNode = isRingLink(headNode);
if(meetNode!=null){
//将其以meetNode分为两个链表,转化为求两个链表的第一个相交的节点。
Node newHeadNode = meetNode.getNextNode();
meetNode.setNextNode(null);
//两个链表 headNode,newHeadNode
return getFirstSameNode(headNode, newHeadNode);
}
return null;
}
//链表是否有环,若有则返回其中一个节点
public Node isRingLink(Node headNode){
Node firstNode = headNode;
Node lastNode = headNode;
while(firstNode.getNextNode()!=null){

if(firstNode.getNextNode().getNextNode()!=null){
firstNode = firstNode.getNextNode().getNextNode();
}else {
firstNode = firstNode.getNextNode();
}
lastNode = lastNode.getNextNode();
if(firstNode.getValue() == lastNode.getValue()){
//相遇,则说明为环形链表
return firstNode;
}
}
return null;
}
//求两个链表中第一个公共点
public Node getFirstSameNode(Node headNode,Node newHeadNode){
//首先求两个链表的长度
int len = 0;
int len_new = 0;
Node curNode  = headNode;
while(curNode!=null){
len++;
curNode = curNode.getNextNode();
}

curNode = newHeadNode;
while(curNode!=null){
len_new++;
curNode = curNode.getNextNode();
}
//链表长的先走len-len_new步
if(len>len_new){
for(int i=0;i<len-len_new;i++){
headNode = headNode.getNextNode();
}
}else{
for(int i=0;i<len_new-len;i++){
newHeadNode = newHeadNode.getNextNode();
}
}

//同时向前走,直到遇到相同节点
while(headNode.getKey()!=newHeadNode.getKey()){
headNode = headNode.getNextNode();
newHeadNode = newHeadNode.getNextNode();
}
return headNode;
}


11. 以O(1)的时间复杂度删除单链表中某指定节点
以下方法都是基于链表中存在target节点
思路1:一般都是从头遍历找到要被删除节点target的前驱节点h,将h-next指向target-next,然后删除target即可。但是时间复杂度为o(n)
思路2:覆盖法。无需找到target的前驱节点,可以直接用target-next来覆盖target的内容,target-next = target-next-next, 然后删除target-next即可。
特殊情况节点:
* 当target是尾节点时,即没有next,则必须找到其前驱节点。
* 当target即为头结点又是尾节点(唯一节点),则应该删除以后把链表置为null
public void deleteNode(Node head,Node node){
if(node.getNextNode()!=null){
//中间节点
Node nextNode = node.getNextNode();
node.setValue(nextNode.getValue());
node.setNextNode(nextNode.getNextNode());
nextNode = null;
}else if(head == node){
//链表中仅有一个节点
node = null;
head.setValue("");
head = null;
}else {
//target为尾节点
//从头开始遍历,得到target的前驱节点
while(head.getNextNode()!=node){
head = head.getNextNode();
}
head.setNextNode(null);
node = null;
}
}

12.0,1...n-1这n个数围成一个圈,从数字0开始每次从这个圆圈中删除第m个数字,求出圆圈中最后剩下的数字。

思路1:利用环形链表。建立n的环形链表,每次删除第m个节点,时间复杂度为O(nm),空间复杂度为O(n)。此处用数组来模拟
思路2:找数学规律.见《剑指Offer》230页解释.利用得到的递归公式可以很简单的求出n>1时f(n,m)=[f(n-1,m)+m]%n

<span style="color:#333333;"><span style="white-space:pre">	</span>// arr:环形数组,n:数组的个数,m:每次删除第几个

public int findLastRemaining(Node[] arr,int n,int m){
if(arr.length<1||n<1||m<1){
return -1;
}
int start = 0;
Node curNode = arr[start];
Node preNode = null;
while(arr[start]!=arr[start].getNextNode()){
//若相等则说明只剩下一个元素
//先向前走m-1步
for(int i=0;i<m-1;i++){
preNode = curNode;
curNode = curNode.getNextNode();
}
//删除元素
preNode.setNextNode(curNode.getNextNode());
//重新建立连接
start = preNode.getNextNode().getKey();
curNode = arr[start];
}

return arr[start].getKey();
}

public static void main(String[] args){
OfferTest_050 test_050 = new OfferTest_050();
int n = 5;
Node[] arr = new Node
;
//建立环形数组
for(int i=0;i<n;i++){
arr[i] = new Node(i);
}
for(int i=0;i<n;i++){
if(i==n-1){
arr[i].setNextNode(arr[0]);
}else {
arr[i].setNextNode(arr[i+1]);
}
}
int lastRemaining = test_050.findLastRemaining(arr, 5, 3);
System.out.println(lastRemaining);
}</span>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: