2.7.4.3.8.1 找出单链表中的倒数第k个元素,实现该方法
这是一道经典的笔试题,其实实现该方法十分简单。直接找倒数第k个元素似乎不太容易,要想找出倒数第k个元素,我们可以从第k个元素和倒数第k个元素的关系来获取倒数第k个元素。该关系如下所示。
从上图可以看出来,倒数第k个节点的索引 = size – (k+1)。所以方法实现如下:
/**
- @functionName reverseGet
- @description 通过索引获取链表倒数第index+1个节点的数据区数据
- @param index 倒数第index+1个节点的索引
- @author yzh
- @date 2019-02-05
*/
public T reverseGet(int index) {
return getInnerNode(size - (index + 1)).getData();
}
2.7.4.3.8.2 寻找单链表的中间结点,实现该方法
这道笔试题也是比较常见的,只要对链表的结构有一定的认识即可写出实现代码。其实,该题就是获取节点方法的一个变式。需求描述:当单链表节点为偶数个时,取两个中间节点的后一个节点作为中间节点,如果链表为奇数个节点则取中间节点。实际上,我们取到中间节点的索引,然后调用get()方法即可获取到中间节点了。根据理解,中间节点的索引 = size/2。代码实现如下所示。
/**
- @functionName getMidNode
- @description 获取链表中间节点的数据区数据
- @author yzh
- @date 2019-02-15
*/
public T getMidNode() {
return getInnerNode( size/2 ).getData();
}
这也是一道比较常见的笔试题。解题思路:删除重复数据肯定需要用到删除方法,因此首先需要找到重复数据对应节点的索引,然后通过delete(int index)方法删除掉这些重复的节点。单链表有一个比较重要的特点是,由于没有在节点类中定义索引属性,因此要找到某一个节点的索引一般需要从头节点开始遍历获取到目标节点的索引。代码如下所示。
/**
- @functionName deleteRepate
- @description 删除链表重复元素
- @author yzh
- @date 2019-02-16
*/
public void deleteRepate(){
//如果链表的节点少于2则不用删除重复元素
if(size < 2){
return;
}
Node currNode = headNode;
int currPos = 0;
while(currNode != null){
Node nextNode = currNode.getNext();
int nextPos = currPos;
while(nextNode != null){
//考虑元素为null的情况
if(currNode.getData() == null && nextNode.getData() == null){
//删除后面重复的节点,这个方法会使删除节点的前一个节点将指向该删除节点的下一个节点,所以nextPos索引不应增加
//应与后一个节点(将变成当前节点)做比较
deleteByIndex(nextPos + 1);
//相等分为两种情况:1、全部为空 2、全部不为空且相等
}else if(currNode.getData() != null && nextNode.getData() != null && nextNode.getData().equals(currNode.getData())){
//删除后面重复的节点,这个方法会使删除节点的前一个节点将指向该删除节点的下一个节点,所以nextPos索引不应增加
//应与后一个节点(将变成当前节点)做比较
deleteByIndex(nextPos + 1);
}else{
//索引增加,size会变化,所以这里需要在不相等时才+1
nextPos++;
}
//节点位置后移
nextNode = nextNode.getNext();
}
//节点位置后移
currNode = currNode.getNext();
//节点索引增加
currPos++;
}
}
2.7.4.3.8.5 单链表的集合运算
2.7.4.3.8.5.1 交集
交集是集合在数学中的概念,用于描述集合间的公共部分,假设现在有A、B两个集合,那么A与B的交集如下所示。
通过概念,我们知道,交集可以理解为两个集合的公共元素,因此,我们可以通过比较两个集合的元素,将相同的元素插入到交集中。代码实现如下所示。
/**
- @functionName intersect
- @description 求两个集合的交集
- @param newList 另一个集合
- @author yzh
- @date 2019-02-05
*/
public MyLinkedList intersect(MyLinkedList newList){
if( this.isEmpty() || newList == null || newList.isEmpty() ){
return null;
}
//交集
MyLinkedList intersect = new MyLinkedList();
Node firstLoop = this.headNode;
while(firstLoop != null){
Node secondLoop = newList.headNode;
while(secondLoop != null){
//考虑元素为空的情况
if(firstLoop.getData() == null && secondLoop.getData() == null){
//添加元素
intersect.add(firstLoop.getData());
//元素不为空的情况
}else if((firstLoop.getData() != null && secondLoop.getData() != null) && firstLoop.getData().equals(secondLoop.getData())){
//添加元素
intersect.add(firstLoop.getData());
}
secondLoop = secondLoop.getNext();
}
firstLoop = firstLoop.getNext();
}
//元素去重
intersect.deleteRepate();
return intersect;
}
说明:实现两个集合交集时,需要考虑两个集合中元素为空这种特殊情况。因此既需要考虑元素为空的情况,也要考虑元素不为空的情况。为什么要使用范型呢?我们知道范型有一个非常重要的特点,那就是对元素类型进行类型检查约束,看是否是和当前元素的类型保持一致。如果我们不加范型对类型进行约束,那么添加任何类型作为元素都可以,一个集合中如果存在多种类型,那么我们将无法判断这些元素的类型,从而无法进行元素排序、类型判断等操作,因为这些基本操作都是必须明确元素类型的。
2.7.4.3.8.5.2 并集
假设现在有A、B两个集合,那么A与B的并集如下所示。
实现并集比实现交集要简单得多,我们只要将两个集合的元素分别添加到新的List中,然后将该List的元素去重即可。实现的代码如下所示。
/**
- @functionName union
- @description 求两个集合的并集
- @param newList 另一个集合
- @author yzh
- @date 2019-02-05
*/
public MyLinkedList union(MyLinkedList newList){
if(this.isEmpty()){
return newList;
}else if(null == newList || newList.isEmpty()){
return this;
}
//并集
MyLinkedList union = new MyLinkedList();
//添加当前集合的元素
Node currListNode = this.headNode;
while( currListNode != null){
union.add(currListNode.getData());
currListNode = currListNode.getNext();
}
//添加newList的元素
Node newListNode = newList.headNode;
while( newListNode != null){
union.add(newListNode.getData());
newListNode = newListNode.getNext();
}
//对union集合进行元素去重
union.deleteRepate();
return union;
}
2.7.4.3.8.5.1 差集
假设现在有A、B两个集合,那么A与B的差集如下所示。
上图给出的是AB集合交集相对于AB集合并集的补集,思路是,先求出集合A和集合B的并集和交集,再除去并集中的那部分元素,剩下的就是AB集合交集相对于AB集合并集的补集,代码如下所示。
/**
- @functionName differenceSet
- @description 求两个集合的差集
- @param newList 另一个集合
- @author yzh
- @date 2019-02-05
*/
public MyLinkedList differenceSet(MyLinkedList newList){
//1.先求AB集合的并集
MyLinkedList union = this.union(newList);
//2.求AB集合的交集
MyLinkedList intersect = this.intersect(newList);
Node intersectNode = intersect.headNode;
while(intersectNode != null){
//3.去除交集的那部分元素
union.delete(intersectNode.getData());
intersectNode = intersectNode.getNext();
}
return union;
}
2.7.4.3.8.6 判断链表是否有环,如果有环,求头节点到环入口的距离以及环的长度
题目:现有一个节点间逻辑距离均匀的链表,节点与节点之间的逻辑距离为M,判断链表是否有环,如果有环,求头节点到环入口的距离以及环的长度。
这是一道非常经典且有一定难度的面试题,不单单考查了扎实的链表知识,也考查了一定的数学功底。单从题目来看,只给出了节点点逻辑距离M并没有给出其他有用的信息,因此,可以理解为我们的链表的节点数目是不固定的,即依赖于用户的输入。我们先使用图来分析一下这道题。
在上图中,是一个带环的单链表,该链表由头节点到环入口这段链表和环组成,其中,C是环的入口。求A到C的距离,以及起点C到终点C的距离。链表从A点出发,从节点C进入“环”后,会一直在该环内循环:A->B->C->D->E->F->H-C-D->E->F->H->C。因此,要想求出A到C的距离和起点C到终点C的距离,关键是求出节点C。
2.7.4.3.8.6.1 判断链表是否有环
第一步:证明是否为环链
要求出节点C,首先我们要证明该链表有环,证明有环首先要排除该链表为环链,如果最后的节点指向头节点,那么该链表就是环链。因此,证明环链的代码如下所示。
/**
- @functionName hasCircle
- @description 判断是否为单链环
- @author yzh
- @date 2019-03-01
/
public boolean isCircle(){
//当size==1时,表示头节点指向头节点
return size >= 1 && (getInnerNode(this.size - 1).getNext() == this.headNode);
}
第二步:如果不是环链,那么再判断该链是否有环
要判断该非环链是否有环,可以定义一个快指针和一个慢指针,且快指针的速度是慢指针的2倍,怎么理解指针的”快”和”慢”呢?可以用下面的代码来表示:
Node fast = headNode;
Node slow = headNode;
slow = slow.getNext();
fast = fast.getNext().getNext();
如果是单链表,那么慢指针将永远追不上快指针(准确点来说是和快指针相遇);如果存在环,那么链表从A点出发,从节点C进入“环”后,会一直在该环内循环,慢指针和快指针总会在环内某个节点相遇。这样我们就证明了该单链存在环,代码如下所示。
/* - @functionName getFastSlowIntersectNode
- @description 获取快慢指针的交点
- @author yzh
- @date 2019-03-01
*/
public Node getFastSlowIntersectNode(){
//定义快指针
Node fast = headNode;
//定义慢指针
Node slow = headNode;
//如果有环,那么会进行无限循环,如果是单链表则会作为终止条件
while(slow != null){
//当slow == fast时,表示快慢指针相遇,这里需要排除头节点初始化的情况
if(slow != headNode && fast != headNode && (slow == fast)){
//返回快慢指针的交点
return slow;
}
slow = slow.getNext();
if(fast != null && fast.getNext() != null){
fast = fast.getNext().getNext();
}
}
//如果快慢指针没有相交,那么说明该链表没有环,则返回空
return null;
}
/**
- @functionName hasCircle
- @description 判断是链表否有环
- @author yzh
- @date 2019-03-01
*/
public boolean hasCircle(){
//如果链表是一个链环或者链表的快慢指针没有交点说明链表中不存在环,那么返回false
if(isCircle() || (getFastSlowIntersectNode() == null)){
return false;
}
//否则该链表存在环
return true;
}
第三步:求出A到C的距离和起点C到终点C的距离
在上面我们已经求出了快慢指针第一次相交的节点,这是求解问题的关键。现假设慢指针在快慢指针第一次相交时走了len的长度(其中len = Mi),设由节点到环的入口的距离为A,环的入口到快慢指针的交点的距离为X,此时,快指针已经走了环n圈,慢指针走了g圈。则有下面的等式成立:
len = A+X+gr;//慢指针走的距离(慢指针走了g圈)
2len = A+X+nr;//快指针走的距离(指针走了n圈)
两式相减得到len = (n-g)r= A+X+gr;由于A=(n-2g)-X,所以n>2g!而且n-2g一定是正整数,最小值为2g+1,所以可以对n进行讨论,讨论情况及等式如下图所示。
显然,nr表示走了n圈环,因此A的距离表示(n-2g)r+(r-X),这表明距离A等于节点E到节点C的距离再加上n-2g圈环的距离。根据len = (n-g)r可知,当n = 2g+1时,A+X = r,此时表明从快慢指针第一次相交的点到环入口的距离刚好是头节点到环入口的距离。当n>2g+1时,则有A = (n-2g)r-X =(r-X)+(n-2g-1)r ,此时A的长度要大于r-X的长度,且A的长度是(r-X)加上r的整数倍的长度,即不管是哪一种情况,我们都可以这样认为:当节点指针变量y从头节点遍历到节点C,此时从快指针和慢指针的交点出发的节点已经遍历了若干圈环再加上交点到环入口的距离,此时两个节点第一次相遇。因此我们可以在头节点和快慢指针的交点各定义一个指针变量,让这两个节点i,j同时出发,当两个结点第一次相遇时则是环的入口。(注意:由于快指针快于慢指针,快慢指针相交时,快指针至少已经走了一圈,因为第一圈中慢指针永远追不上快指针,但快慢指针不一定是在慢指针的第一圈中相遇!)。因为当n = 2g+1时,节点i在结点j的第一圈中相遇至C处;当n>2g+1时,此时,根据等式A = (n-2*g)r-X =(r-X)+(n-2g-1)r,节点i在结点j的第(n-2g-1)圈后第一次相遇至C处。不管是哪种情况,都会相交于C处。因此,我们可以分别在头节点位置和快慢指针相遇的位置分别定义一个指针变量,然后同时向前循环,当两个指针变量第一次重合时就是环的入口。代码实现如下所示。
/**
-
@functionName getCircleEntrance
-
@description 获取链表环入口
-
@author yzh
-
@date 2019-03-02
*/
public Node getCircleEntrance(){
//1.如果链表没有环或链表为空链表,那么返回空
if(!hasCircle() || size == 0){
return null;
}//2.定义指向头节点、快慢指针第一次相交的指针变量
Node headNodeM = this.headNode;
Node intersectNodeN = getFastSlowIntersectNode();while(headNodeM != null && intersectNodeN != null){
//3.如果两个节点第一次相交,那么就是环的入口
if(headNodeM == intersectNodeN){
return headNodeM;
}
//让它们同时向前循环
headNodeM = headNodeM.getNext();
intersectNodeN = intersectNodeN.getNext();
}
return null;
}
2.7.4.3.8.6.2 构建带环的链表
当然,为了验证上面代码的正确性,这里我们需要构建一个有环的链表,构建有环的链表的规则如下所示。
- 如果是第一次添加节点,那么就直接让headNode=node。
- 如果是第二次添加节点,那么就先让头节点的指针指向新节点,然后让新节点的指针指向自己(单节点环)。
- 如果是第三次添加节点,那么就让倒数第二个节点指向新节点,让新节点指向自己。
- 如果是第4次或第四次以上添加节点,先 获取第三个节点作为环的入口,当节点的指针指向环入口的时候,该节点就是添加新节点的临界点(排除第二个节点),再让当前节点的指针指向新节点,最后让新节点的指针指向环入口。代码如下所示。
/**
- @functionName addCircle
- @description 添加有环的链表
- @param data 链表元素
- @author yzh
- @date 2019-03-02
*/
public void addCircle(T data){
Node node = new Node();
node.setData(data);
//1.如果是第一次添加节点,那么就直接让headNode=node
if(headNode == null){
headNode = node;
//2.如果是第二次添加节点,那么就先让头节点的指针指向新节点,然后让
//新节点的指针指向自己(单节点环)
}else if(size == 1){
headNode.setNext(node);
//指向自己
node.setNext(node);
//3.如果是第三次添加节点,那么就让倒数第二个节点指向新节点,让新节点指向自己
}else if(size == 2){
Node temp = headNode.getNext();
//添加新节点(指向新的节点)
temp.setNext(node);
//让新节点指向自己
node.setNext(node);
//4.如果是第4次或第四次以上添加节点
}else{
//4.1获取第三个节点作为环的入口
Node circleEntrance = headNode.getNext().getNext();
Node temp = headNode.getNext().getNext();
//4.2当节点的指针指向环入口的时候,该节点就是添加新节点的临界点(排除第二个节点)
while(temp.getNext() != circleEntrance){
temp = temp.getNext();
}
//4.3指针指向新节点
temp.setNext(node);
//4.4让新节点的指针指向环入口
node.setNext(circleEntrance);
}
size++;
}
2.7.4.3.8.6.3 求带环链表头节点到环入口的距离以及环的长度
在上面我们已经获取到了环入口节点,因此,求头节点到环入口的距离以及环的长度就变得十分简单了。对于环的长度,就是环入口到下一个环入口节点位置的长度。代码实现如下所示。
/**
- @functionName getHeadToCircleEntranceLength
- @description 获取头节点到环入口的长度
- @param nodeLength 链表节点间的单位逻辑长度
- @author yzh
- @date 2019-03-02
/
public int getHeadToCircleEntranceLength(int nodeLength){
//如果没有环或传入异常的节点间的单位逻辑长度,则不存在距离
if(!hasCircle() || nodeLength <= 0){
return -1;
}
Node temp = headNode;
//获取第三个节点作为环的入口
Node circleEntrance = this.getCircleEntrance();
//节点间间隔数
int count = 0;
while(temp != circleEntrance){
temp = temp.getNext();
count++;
}
return countnodeLength;
}
/**
- @functionName getCircleLength
- @description 获取头节点到环入口的长度
- @param nodeLength 链表节点间的单位逻辑长度
- @author yzh
- @date 2019-03-02
*/
public int getCircleLength(int nodeLength){
//如果没有环或传入异常的节点间的单位逻辑长度,则不存在距离
if(!hasCircle() || nodeLength <= 0){
return -1;
}
//获取第三个节点作为环的入口
Node circleEntrance = this.getCircleEntrance();
Node temp = circleEntrance;
//节点间间隔数
int count = 0;
while(temp.getNext() != circleEntrance){
temp = temp.getNext();
count++;
}
//环节点间节点间数需要+1
return (count + 1) * nodeLength;
}
测试:
MyLinkedList myLinkedList7 = new MyLinkedList();
myLinkedList7.addCircle(1);
myLinkedList7.addCircle(2);
//myLinkedList7.showAllNodes(myLinkedList7.headNode);//1->2->2->2…
myLinkedList7.addCircle(3);
//myLinkedList7.showNodes(myLinkedList7.headNode);//1->2->3->3->3…
myLinkedList7.addCircle(4);
myLinkedList7.addCircle(5);
myLinkedList7.addCircle(6);
myLinkedList7.addCircle(7);
System.out.println(“头节点到环入口的长度:”+myLinkedList7.getHeadToCircleEntranceLength(3));
System.out.println(“环的长度:”+myLinkedList7.getCircleLength(3));
/**output:
头节点到环入口的长度:6
环的长度:15
*/