线性表–单链表(二)
1、链表数据结构
typedef struct node {
data_t data;
struct listnode *next;
}linknode, *linklist;
2、如何找出链表的第K个元素?
我们在日常的思维中,输入一个链表并输出其第K个元素符合我们的习惯,那么倒数第K个结点该怎么找到呢?
linklist getKthFromEnd(linklist head, int k) {
linklist former = head;
linklist latter = head;
int i = 0;
for (i = 0; i < k; i++) {
former = former->next;
}
while (former) {
latter = latter->next;
former = former->next;
}
return latter;
}
算法思路:
快慢指针,先让快指针走K步,然后两个指针同步走,当快指针走到头时,慢指针就是链表倒数第K个结点。
①实际的思路,其实就是两个指针相互相隔K个元素,此时二者以相同的间隔移动到链表的最后,此时慢指针也就成为了链表的倒数第K个结点。
②当然如果链表不长的话,也可以通过先遍历链表所有元素,并记录元素个数N。然后从开头移动到N-K+1位置的数据域即为所找元素。
3、如何找出链表的中间结点
给定一个带有头结点head的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
linklist middleNode(linklist head) {
linklist sloweer = head;
linklist faster = head;
while ((faster != NULL) && (faster->next != NULL)) {
faster = faster->next->next;
slower = slower->next;
}
return slower;
}
算法思路:这是一个典型的双指针问题,定义一个快指针,一个慢指针。快指针一次走两步,慢指针一次走一步。当快指针走到结尾时,慢指针指向的就是中间结点。
注:①这里注意一个细节:题目中“若有两个中间结点,则返回第二个中间结点”。此时可以在草稿纸上写写画画,就拿自己74的左右手的两根指头同步移动,可以得出:快指针可以前进的条件是:当快指针和当前快指针的下一个结点都非空。
②如果题目要求“在两个中间结点的时候,返回第一个中间结点”,此时,快指针可以前进的条件是:当前快指针的下一个结点和当前快指针的下下一个结点都非空。
4、反转链表
在上一篇中,我们已经写了反转链表的相关算法,但那个是一般的“新链表头部插值法”,这里我们再提出一种递归的方法。
linklist reverselist (linklist head) {
if (head == NULL || head->next == NULL) {
return head;
}else {
linklist mid = head;
linklist latter = mid->next;
head = reverselist(latter);
latter->next = mid;
mid->next = NULL;
return head;
}
}
①反转链表实际就是重新指向结构体中的next指针。我们需要修改下一个结点的next指针指向前一个结点。所以在遍历链表时候我们需要逐个修改链表的指针指向。这个题目也可以用递归来做,一直递归到链表的最后一个结点,该节点就是反转后的头结点,记作head。
②此后,每次函数在返回的过程中,让当前结点的下一个结点的next指针指向当前结点。同时,让当前结点的next指针指向null,从而事项从链表尾部开始的局部反转。当递归函数全部出栈后,链表反转完成。
③递归程序怎么设计?
递归程序我们可以看做是嵌套的程序,但嵌套过程繁杂,我们怎么简洁地区考虑呢?这里提出一点我自己的看法,以上述程序为例,总的可以分为三部分,第一部分为linklist mid = head; linklist latter = mid->next;,中间部分为head = reverselist(latter); 结尾部分为latter->next = mid;mid->next = NULL;return head;因而我们可以这么认为,中间部分递归函数的之前部分,即第一部分为“递归进入”执行程序,而递归函数后,即最后一部分为 “递归返回”执行程序,也就是函数通过递归函数,执行N遍第一部分,然后再执行N遍最后一部分。最后达到算法效果。当然需要考虑递归的特点,即其中的变量随着函数一层层进入和返回,是不断调整和变化的。
5、环形链表
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回NULL。
linklist detect_cycle(linklist head) {
linklist slow = head;
linklist fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
fast = head;
while (slow !=fast) {
slow = slow->next;
fast = fast->next;
}
return slow;
}
return NULL;
}
解题思路:
假设有两个指针,分别为快慢指针fast和slow,快指针每次走两步,慢指针每次前进一步,如果有环则两个指针必定相遇;
A:链表起点
B:环起点
C:相遇点
X:环起点到相遇点距离
Y:链表起点到环起点距离
R: 环的长度
s:第一次相遇时走过的路程
慢指针slow第一次相遇走过的路程 S1 = Y + X: ( 1)
快指fast第一次相遇走过的路程 S2=2S1 = Y + X + NR;(2)
说明:快指针的速度是慢指针的两倍,相同时间内路程应该是慢指针的两倍,Y +X+ NR是因为快指针可能经过N圈后两者才相遇:
把(1)式代入(2)式得 : Y = NR -X:表明头节点和C点的慢指针会在B点相遇,此处就是环节点
5、单链表相交,如何求交点?
给定两个( 单向)链表,判定它们是否相交并返回交点。请注意相交的定义基于节点的引用,而不是基于节点的值。换句话说,如果一个链表的第k个节点与另一个链表的第i个节点是同一节点(引用完全相,则这两个链表相交。
linklist get_intersection_node(linklist headA, linklist headB) {
linklist p = headA;
linklist q = headB;
while (p != q) {
if (p == NULL) {
p = headB;
}else {
p = p->next;
}
if (q == NULL) {
q = headA;
}else {
q = q->next;
}
}
return q;
}
算法思路:
根据题意,两个链表相交的点是指: 两个指针指向的内容相同,则说明该结点记在A链表上又在B链表上,进而说明A和B是相交的而对于相交的情况,两条链表一定是这种结构:
为什么呢?
因为如果链表A和链表B相交于D的话,那么说明D结点即在A上又在B上,而D之后的元素自然也就均在A和B上了,因为他们是通过next指针相连的.
如果有相交的结点D的话,每条链的头结点先走完自己的链表长度,然后回头走另外的一条链表,那么两结点定为相交于D点,因为这时每个头结点走的距离是一样的,都是 AD + BD + DC,而他们每次又都是前进1,所以距离相同,速度又相同,固然一定会在相同的时间走到相同的结点上,即D点。