单链表算法题

绪论:

针对部分算法题其算法阐述思路

1、移除链表元素:

示例:

思路:

1、

针对移除链表中某个节点,可以创建一个新的节点来一一接收旧节点中节点,如下图所示:

创建两个新节点 newhead、ptail 初始值为 NULL 

2、

       起始时,若旧节点中的 pcur->next 不等于 val(设为条件1) ,则将 pcur 赋给 newphead与ptai,pcur指向下一个节点,这样就完成了第一次循环。 此时pcur指向第二个节点,ptail与newphead指向第一个节点。

        第二次进入循环时,旧节点仍满足条件1,首先 ptail->next 指向 pcur,再将ptail移向下一节点,同时pcur移向下一节点,如下图所示。

三次循环时,当前pcur指向节点不满足条件1,所以直接将pcur指向下一节点,ptail不变,如此循环直至末尾

3、

当pcur指向NULL时,跳出循环,此时新节点中,最后一个节点指向的地址要置为NULL。如下图所示

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* newphead ,*ptail;
    newphead = ptail = NULL;
    ListNode* pcur;
    pcur = head;
  
    while(pcur)
    {  
        if(pcur->val != val)
        {
            if(ptail == NULL)
            {
                newphead = ptail = pcur;//我怎么会觉得单改变改变ptail就会改变newphead的?
            }                           //这两个同为同一类型的指针变量,不存在传址调用的那层关    
                                        //系
            else
            {
                ptail->next = pcur;
                ptail = ptail->next;
            }
        }
        pcur = pcur->next;
    }
    if(ptail)                           //至于为什么是tail 而不是ptail->next
    {                                   //当要删除所有旧节点时,那么新节点全为NULL指针
        ptail->next = NULL;             //因此是ptail,不要想当然去用ptail->next
    }                                   //仔细思考一下,其实前者作为条件更好。
    return newphead;
}

2、反转链表:

示例:

思路:

1、

创建三个结构体指针n1、n2、n3,其指向如下图所示:

注:n3指针的指向前要判断节点是否为NULL

2、

我们将n2->next指向n1,n2 赋给 n1,此时 1 和 2 节点已经断开联系,但可以通过 n3 指针,让 1 和 2 节点重新建立联系,即令 n2 =n3 ,这样 n2 便可以通过赋值的方式 指向下一个节点,n3 则通过链表方式,指向下一个节点。如下图所示:红色为赋值后节点。

3、

重复上述循环,直至n2为NULL时,跳出循环,此时,n1处于尾节点,也是新链表的头节点。如下图所示

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    ListNode* n1 = NULL;
    ListNode* n2 = head;
    if(head == NULL)    
    {
        return head;
    }
    
    ListNode* n3 = head->next;//不能对空指针进行结构体访问操作
    while(n2)
    {
        n2->next = n1;
        n1=n2;
        n2=n3;
        if(n3)             //当n3为空时,此时循环尚未结束,因此最后一次结构体访问操作不能正常运行
        n3=n3->next;
    }
    return n1;
}

3、寻找链表的中间节点:

示例:

思路:

此处涉及一个重要算法思路:快慢指针。

我们来举两个例子分析一下快慢指针:

例1:

首先定义两个结构体指针 fast 和 slow ,其中 slow 每次移动一个节点大小,fast 移动两个,最终,当fast移动到尾部时,此时slow正好在中间位置(节点数为奇数)。注:红色为移动后指针位置。

例2:

对于总结点个数为偶数个数时,slow正好处于中间节点的下一个位置处,如图所示:

注:对于总结点个数为偶数时,此时fast指向NULL

对于快慢指针,快指针运动的路程为2n,而慢指针运动的路程为n,因此慢指针总是指向中间位置,或者中间位置靠后的第一个位置。

根据上述思想,代码编写就很容易了。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
    ListNode* fast,*slow;
    fast = slow = head;
    while(fast && fast->next)        //注意短路:不能先把fast->next的条件写在前面
    {                                //我们要先判断fast不能为空,才能去判断fast->next不能空
        slow = slow->next;           //因为空指针不能进行结构体成员访问操作
        fast = fast->next->next;
    }
    return slow;
}

4、合并两个有序链表

示例:

思路:

思路与合并两个有序数组相类似,创建两个结构体指针分别指向两个链表,遍历比较,将较小的那个尾插到新节点当中。如图所示:

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if(list1 == NULL)        //考虑到链表为空,优先判断,再执行后续操作可以优化程序
    {
        return list2;
    }
    if(list2 == NULL)
    {
        return list1;
    }

    ListNode* p1 = list1;
    ListNode* p2 = list2;
    ListNode* p3,*newhead;
    newhead = p3 = (ListNode*)malloc(sizeof(ListNode));
                            //如果用动态内存开辟,返回的是同一个区域,我们对p3修改的同时也会 
                            //对newhead修改
    while(p1 && p2)         //
    {
        if(p1->val <= p2->val)
        {
            p3->next = p1;
            p1 = p1->next;
        }
        else
        {
            p3->next = p2;
            p2 = p2->next;
        }
        p3 = p3->next;
    }

    if(p1)
    {
        p3->next = p1;
    }
    if(p2)
    {
        p3->next = p2;
    }
    ListNode* tmp = newhead->next;
    free(newhead);
    newhead = NULL;

    return tmp;
}

注:

小插曲1:

为什么用动态内存开辟新的头节点呢?这是为了后续编程中,不用考虑头节点为空的情况,如果头节点是空,那么还得对新的头节点做一次判断,编程会麻烦些。其次,用动态内存开辟的新节点,有效数据是从新节点的next即,newnode->next 开始的,因此在编程过程中,只需在代码末提前保存 newnode->next 的值,并将临时节点释放,返回tmp即可。这一点在例题5中也有体现。

小插曲2:

 newhead = p3 = (ListNode*)malloc(sizeof(ListNode));此处 newhead 和 p3 是指向同一块内存区域

所以说,对p3进行修改会作用到newhead上,与移除链表元素示例中的代码不同,动态内存开辟函数会将同一个地址传递给这两个参数,对这两个变量进行解引用或者结构体成员访问操作时,实质是在同一个地址上进行操作,因此两个变量会一同变化。

证明如下:

5、链表分割:

示例:

思路:

用动态内存函数创建两个结构体指针:smallhead 与 bighead,遍历链表,将小于x的赋到 smallhead 中,大于 x 的赋到 bighead 中,最后将 smallhead 的尾节点与 bighead 的头节点连接。如下图所示

需要注意的点:若 bighead 的尾节点不指向NULL,会存在环形链表的情况,即死循环,如下图所示

代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
     
        ListNode* smallhead,*bighead;
        smallhead  = (ListNode*)malloc(sizeof(ListNode));
        bighead = (ListNode*)malloc(sizeof(ListNode));
        ListNode* stail = smallhead;
        ListNode* btail = bighead;
        ListNode* pcur = pHead;

        while(pcur)
        {
            if(pcur->val < x)
            {
                stail->next = pcur;
                stail = stail->next;
            }
            else
            {
                btail->next = pcur;
                btail = btail->next;
            }
            pcur = pcur->next;
        }
        
        btail->next = NULL;   //大链表尾节点置为NULL,避免死循环
        
        stail->next = bighead->next;
        ListNode* tmp = smallhead->next;
        free(smallhead);
        free(bighead);
        smallhead = NULL;
        bighead = NULL;
        return tmp;

    }
};

6、链表的回文结构:

示例:

思路:

先通过快慢指针找到中间节点 mid,再从中间节点开始,向后逆置链表,最后头尾分别向右向左遍历依次比较判断。如下图所示:

代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
    ListNode* findmidnode(ListNode* head) //找到中间节点mid
    {
        ListNode* fast,*slow;
        fast = slow = head;
        while(fast && fast->next)
        {
            fast=fast->next->next;
            slow=slow->next;
        }
        return slow;
    }

    ListNode* reversenode(ListNode* mid)//从中间节点位置处逆置链表
    {
        ListNode* n1,*n2,*n3;
        n1 = NULL;
        n2 = mid;
        n3 = mid->next;
        while(n2)
        {
            n2->next = n1;
            n1 = n2;
            n2 = n3;
            if(n3)
            n3 = n3->next;
        }
        return n1;
    }
    bool chkPalindrome(ListNode* A) {
        // write code here
        //找中间节点
        ListNode* mid = findmidnode(A);
        //从中间节点开始逆置
        ListNode* ptail = reversenode(mid);
        //头尾依次比较
        ListNode* pcur = A;
        while(ptail)
        {
            if(pcur->val != ptail->val)
            {
                return false;
            }
            pcur = pcur->next;
            ptail = ptail->next;
        }
        return true;
    }
};

7、相交链表的判断

示例:

思路:

遍历 A 与 B 两个链表,统计其节点个数,并做差得 gap ,将长的链表先移动 gap 个节点位置,然后遍历每个链表依次比较,指针相等或链表指向NULL;

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    ListNode* p1 = headA;
    ListNode* p2 = headB;
    int a,b;
    a = b = 0;
    while(p1)            //统计链表 A 中节点个数
    {
        a++;
        p1 = p1->next;
    }
    while(p2)            //统计链表 B 中节点个数
    {    
        b++;
        p2 = p2->next;
    }
    int gap = abs(a - b);
    ListNode* longhead = headA;
    ListNode* shorthead = headB;
    if(a<b)
    {
         longhead = headB;
         shorthead = headA;
    }
    while(gap--)        //让长链表与短链表齐平
    {
        longhead = longhead->next;
    }
    while(longhead)    
    {
        if(longhead == shorthead)    //判断是存在相同节点
        {
            return longhead;
        }
        longhead = longhead->next;
        shorthead = shorthead->next;
    }
    return NULL; 
}

8、环形链表Ⅰ

示例:

思路:

通过快慢指针可以判断一个链表是否为环形链表,因为快指针始终可以追上慢指针。假设快指针与慢指针之间得距离为N,每一次移动,两者间的距离减小1,因此最终N总会变为0,即快指针追上慢指针。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {
    ListNode* fast,*slow;
    fast = slow = head;
    while( fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return true;//当满足条件时,跳出循环
        }
    }
    return false;
}

9、环形链表Ⅱ

示例:

思路:

        要实现此程序,首先要知道:假设快慢指针第一次相遇的位置为 meet,此时若有一个指针 pcur1 从头开始,每次移动一个节点位置;另一个指针 pcur2 从meet位置开始移动,每次移动一个节点位置,则他们两个必定会在入环节点相遇。

        从数学上证明如下图:

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;

ListNode* findmidnode(ListNode* head)    //找快慢节点相遇节点处
{
    ListNode* fast,*slow;
    fast = slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
        {
            return fast;                //返回相遇节点
        }
    }
    return NULL;                        //若为非环状链表 返回null
}

struct ListNode *detectCycle(struct ListNode *head) {
    ListNode* meet = findmidnode(head);
    if(meet == NULL)
    {
        return NULL;
    }
    ListNode* pcur1,*pcur2;
    pcur1= head;
    pcur2= meet;
    while(pcur1 !=pcur2)                //二者相遇出即为入环处
    {
        pcur1 = pcur1->next;
        pcur2 = pcur2->next;
    }
    return pcur1;
}

10、随即链表的复制

示例:

思路:

1:断开原链表连接方式,建立新的连接关系,如下图所示,此处需要开辟新的节点。

2:变更新节点中 random 的指向,如图所示,可以通过新建立的链表关系访问先前节点中的random

3:断开新旧链表间的联系,连接新链表,如图所示:

代码:

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node node;

node* buynewnode(int x)            //申请新的节点并初始化节点
{
    node* Node = (node*)malloc(sizeof(node));
    Node->val = x;
    Node->random = Node->next = NULL;
    return Node;
}

struct Node* copyRandomList(struct Node* head) {
    node* pcur = head;
    if(pcur == NULL)
    {
        return pcur;
    }
    while(pcur)                    //尾插新的节点
    {
        node* newnode = buynewnode(pcur->val);
        newnode->next = pcur->next;
        pcur->next = newnode;
        pcur = newnode->next;
    }

   
    pcur = head;
    while(pcur)                    //将新节点的random赋值
    {
         node* copy = pcur->next;
        if(pcur->random != NULL)
        {
           copy->random = pcur->random->next;
        }
        pcur = copy->next;
    }
    
    pcur = head;
    node* newhead = pcur->next;
    node* ptail = newhead;
    while(ptail->next)            //断开新旧节点的关系,画图比较为什么条件为ptail->next;
    {
        pcur = ptail->next;
        ptail->next = pcur->next;
        ptail = ptail->next;
    }
    return newhead;
}

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值