链表算法经典题

由于我们在讲解的过程中会设计无环单链表和有环单链表,那么我们先创建两个链表

/*链表*/struct ListNode {    int data;    
struct ListNode *next;    
struct ListNode *pre;
};
复制代码

无环链表

struct ListNode* constructList(void)
{   //头结点定义    
struct ListNode *head = NULL;    
//记录当前结点    
struct ListNode *cur = NULL;        
for (int i = 0; i < 8; i++) { 
     struct ListNode *node = malloc(sizeof(struct ListNode));  
     node->data = i;        
//头结点为空,新结点即为头结点        
  if (head == NULL) {            
   head = node;       
 } else {//当前结点的next为新结点         
  cur ->next = node;       
 }       
 //设置当前结点为新结点       
 cur = node;    
}   
 return head;
}
复制代码

有环链表

struct ListNode* constructCycleList(void)
{
    //头结点定义
    struct ListNode *head = NULL;
    //记录当前尾结点
    struct ListNode *cur = NULL;
    //环结点入口
    struct ListNode *cycle = NULL;
    
    for (int i = 0; i < 8; i++) {
        struct ListNode *node = malloc(sizeof(struct ListNode));
        node->data = i;
        if (i == 3) {//第4个为环入口
            cycle = node;
        }
        //头结点为空,新结点即为头结点
        if (head == NULL) {
            head = node;
        } else {//当前结点的next为新结点
            cur->next = node;
        }
        //设置当前结点为新结点
        cur = node;
        if (i == 7) {
            cur->next = cycle;
        }
    }
    return head;
}
复制代码

1. 链表的倒数第K个节点

问题描述:

输入一个链表,输出该链表中倒数第k个结点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点。例如一个链表有6个结点,从头结点开始它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个结点是值为4的结点,需要保证时间复杂度。

算法思路:

设置两个指针p1,p2,从头到尾开始出发,一个指针先出发k个节点,然后第二个指针再进行出发,当第一个指针到达链表的节点的时候,则第二个指针表示的位置就是链表的倒数第k个节点的位置。

代码如下:

struct ListNode* findKth(struct ListNode *head, int k)
{
    struct  ListNode* cur = head;
    struct ListNode*  now = head;
    int i = 0;
    while (cur!=NULL&&i++<k) {
        cur = cur->next;
    }
    while (cur!=NULL) {
        now =now->next;
        cur =cur->next;
    }
    return now;
}
复制代码

总结:当我们用一个指针遍历链表不能解决问题的时候,可以尝试用两个指针来遍历链表。可以让其中一个指针遍历的速度快一些(比如一次在链表上走两步),或者让它先在链表上走若干步。

2.从尾到头打印链表(递归和非递归)

问题描述:

输入一个单链表链表,从尾到头打印链表每个节点的值。输入描述:输入为链表的表头;输出描述:输出为需要打印的“新链表”的表头。

算法思路:

首先我们想到从尾到头打印出来,由于单链表的查询只能从头到尾,所以可以想出栈的特性,先进后出。所以非递归可以把链表的点全部放入一个栈当中,然后依次取出栈顶的位置即可。

代码如下:(递归方法)

struct ListNode* reverseList(struct ListNode* head)
{
    if (head == NULL || head->next == NULL) return head;
    struct ListNode *nextNode = head->next;
    struct ListNode *reverseNode =reverseList(nextNode);
    nextNode->next = head;
    head->next = NULL;
    return reverseNode;
}
复制代码

3.如何判断一个链表有环

问题描述:

有一个单向链表,链表当中有可能出现“环”,如何用程序判断出这个链表是有环链表? 不允许修改链表结构。时间复杂度O(n),空间复杂度O(1)。

算法思路:

首先创建两个指针1和2,同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。

说明 :在循环的环里面,跑的快的指针一定会反复遇到跑的慢的指针 ,比如:在一个环形跑道上,两个运动员在同一地点起跑,一个运动员速度快,一个运动员速度慢。当两人跑了一段时间,速度快的运动员必然会从速度慢的运动员身后再次追上并超过,原因很简单,因为跑道是环形的

代码如下:

bool isLoopList(struct ListNode* head)
{
    //使用快慢指针,慢指针每次向前一步,快指针每次两步
    struct ListNode* slowPoint = head;
    struct ListNode* fastPoint = head;
    while (fastPoint != NULL && fastPoint->next != NULL) {
        slowPoint = slowPoint->next;
        fastPoint = fastPoint->next;
        if (fastPoint->next != NULL) {
            fastPoint = fastPoint->next;
            //两指针相遇则有环
            if (slowPoint == fastPoint) {
                return true;
            }
        }
    }
    return false;
}
复制代码

4.链表中环的大小

问题描述

有一个单向链表,链表当中有可能出现“环”,那么如何知道链表中环的长度呢?

算法思路

由如何判断一个链表有环可以知道,快慢指针可以找到链表是否有环存在,如果两个指针第一次相遇后,第二次相遇是什么时候呢?第二次相遇是不是可以认为快的指针比慢的指针多跑了一个环的长度。所以找到第二次相遇的时候就找到了环的大小。

代码如下

//首先找出相遇时候的环节点,然后再次到此节点则表示环大小
struct ListNode* cycleNode(struct ListNode* head)
{
    //使用快慢指针,慢指针每次向前一步,快指针每次两步
    struct ListNode* slowPoint = head;
    struct ListNode* fastPoint = head;
    while (fastPoint != NULL && fastPoint->next != NULL) {
        slowPoint = slowPoint->next;
        fastPoint = fastPoint->next;
        if (fastPoint->next != NULL) {
            fastPoint = fastPoint->next;
            //两指针相遇则有环
            if (slowPoint == fastPoint) {
                return slowPoint;
            }
        }
    }
    return NULL;
}


int getCycleLength(struct ListNode *head)
{
    
    struct ListNode *node = cycleNode(head);
    //node为空则代表链表无环
    if (node == NULL) {
        return 0;
    }
    int length = 1;
    struct ListNode* currentNode = node->next;
    //再次相遇则循环结束
    while (currentNode != node) {
        length++;
        currentNode = currentNode->next;
    }
    printf("cycle length is %d",length);
    return length;
}
复制代码

5.链表中环的入口结点

问题描述

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

算法思路

如果链表存在环,那么计算出环的长度n,然后准备两个指针pSlow,pFast,pFast先走n步,然后pSlow和pFase一块走,当两者相遇时,即为环的入口处;所以解决三个问题:如何判断一个链表有环;如何判断链表中环的大小;链表中环的入口结点。实际上最后的判断就如同链表的倒数第k个节点。

代码如下

struct ListNode* cycleListFirstNode(struct ListNode* head)
{
    struct ListNode* cur = head;
    struct ListNode* now = head;
    int cycleLength = getCycleLength(head);
    int i = 0;
    while (cur != NULL && i++ < cycleLength) {
        cur = cur->next;
    }
    while (cur != now) {
        now = now->next;
        cur = cur->next;
    }
    printf("cycle 入口 node value  is %d",now->data);
    return now;
}
复制代码

猜你喜欢

转载自juejin.im/post/7018857263263662087