欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

leetcode【数据结构简介】《链表》卡片——经典问题

程序员文章站 2024-02-29 08:20:34
...

反转链表

如何反转一个单链表?

一种解决方案是按原始顺序迭代结点,并将它们逐个移动到列表的头部。
算法概述传送门

在该算法中,每个结点只移动一次。

因此,时间复杂度为 O(N),其中 N 是链表的长度。我们只使用常量级的额外空间,所以空间复杂度为 O(1)

这个问题是你在面试中可能遇到的许多链表问题的基础。我们将在后面更多地讨论实现细节。

当然还有许多其他的解决方案。我们应该熟悉至少一个解决方案并能够实现它。

相关编程题

1. 反转链表

反转一个单链表。

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

【进阶】可以迭代或递归地反转链表。能否用两种方法解决这道题?

方法一:“换头术”

思路:
使用了上文所述的反转单链表的方法。我将之戏称为“换头术”:记录原本头(节点)的位置pos=head,不停地将下面第一个备用的头pos->next作为新的头放到现有头节点head的上方(作为其前继),直到无头可换pos->next==NULL,结束循环。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL || head->next==NULL) return head;
    struct ListNode* cur, * pos=head;
    while(pos->next!=NULL){
        cur = pos->next;
        pos->next = cur->next;
        cur->next = head;
        head = cur;
    }
    return head;
}

leetcode【数据结构简介】《链表》卡片——经典问题

方法二:迭代

思路:遍历链表时,将当前节点的next指针改为指向前一个节点原来是指向后一个节点)。
实现技巧:可以用两个变量,一个存储当前位置,一个存储当前位置的前继。因为如果不存储当前节点的前继,遍历到当前位置时,就找不到他的前继身在何方!这是由于单链表的性质决定的。最后返回新的头引用

方法三:递归

比起迭代,递归就相当骚了~

单单使用文字,并不能很好理解递归算法。所以这里给出一个传送门,里面用文字+动态图的方式,清晰明了说明了递归的原理。(PS:里面的动态图是关键中的关键!建议配合代码里的注释一起食用~)

struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL || head->next==NULL) return head;
    struct ListNode* cur = reverseList(head->next);
    head->next->next = head;
    head->next = NULL;
    return cur;
}

2. 移除链表元素

删除链表中等于给定值 val 的所有节点。

输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val){
    while(head!=NULL && head->val==val){
        struct ListNode* cur=head;
        head = cur->next;
        free(cur);
    }
    if(head==NULL) return NULL;
    if(head->next==NULL) return head;

    struct ListNode* pos=head;
    while(pos->next!=NULL){
        if(pos->next->val==val){
            struct ListNode* cur=pos->next;
            pos->next = cur->next;
            free(cur);
        }
        else pos = pos->next;
    }
    return head;
}

leetcode【数据结构简介】《链表》卡片——经典问题

注意:在写链表的程序的时候,不要对空指针进行比较之类的操作。否则会报错如下:
Line 23: Char 21: runtime error: member access within null pointer of type 'struct ListNode' (solution.c)
切记!

3. 奇偶链表

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL

【说明】应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。

滑动窗口法

思想:
一个快指针一个慢指针,快指针fast需要前移的节点的前继(就是要被前移节点的前一个节点)处停留,申请cur(struct ListNode* cur;)存储要被前移的节点,塞到慢指针slow节点的后面。的过程中需要完成链表的连接工作。

此算法使用原地算法完成。空间复杂度为O(1)。

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

struct ListNode* oddEvenList(struct ListNode* head){
    if(head==NULL || head->next==NULL || head->next->next==NULL) return head;
    struct ListNode* slow=head, * fast=head, * cur=head;
    while(fast!=NULL && fast->next!=NULL && fast->next->next!=NULL){
        fast = fast->next;
        cur = fast->next;
        fast->next = cur->next;

        cur->next = slow->next;
        slow->next = cur;
        slow = slow->next;
    }
    return head;
}

leetcode【数据结构简介】《链表》卡片——经典问题

辅助链表法

思路:将奇节点放在一个链表里,偶链表放在另一个链表里。然后把偶链表接在奇链表的尾部。

此法不如前面的方法,实现起来更加简单,也更加符合直觉思路,故不再赘述。

PS: 其实我想到的第一个方法就是滑动窗口法,虽然说有题目条件“原地算法”的干扰,但是我们也不应该忽略到辅助链表这种最基础的解法。切记!切记!

4. 回文链表

请判断一个链表是否为回文链表

输入: 1->2
输出: false

输入: 1->2->2->1
输出: true

法一:辅助数组+双指针

思路:
申请一个辅助数组存储链表中的所有节点的val值。
判断辅助数组是否为回文数组:使用头尾双指针,一个从头遍历,一个从尾遍历,进行比较判断。

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


bool isPalindrome(struct ListNode* head){
    if(head==NULL || head->next==NULL) return true;    //特例判断
    
    int ArraySize=0;
    struct ListNode* pos=head;
    while(pos->next!=NULL){
        ArraySize++;
        pos = pos->next;
    }
    ArraySize++;
    
    int* array = (int *)malloc(sizeof(int) * ArraySize);
    pos = head;
    int i=0;
    while(pos->next!=NULL){
        array[i++] = pos->val;
        pos = pos->next;
    }
    array[i] = pos->val;
    int left=0, right=ArraySize-1;
    while(left<right){
        if(array[left]!=array[right]) break;
        left++; right--;
    }
	free(array);
    if(left==right || left==right+1) return true;
    else return false;
}

leetcode【数据结构简介】《链表》卡片——经典问题

【注意】
动态数组,其创建麻烦,使用完必须由程序员自己通过free或者delete释放,否则严重会引起内存泄露。
静态数组在内存中位于栈区,是在定义时就已经在栈上分配了固定大小,在运行时这个大小不能改变,在函数执行完以后,系统自动销毁。free(array);(PS:养成好习惯总是不会错~~)
【另外】至于使用动态数组或者是静态数组,就看你是要时间换空间还是空间换时间了~

从执行结果看,这程序算不得好,执行时间长,也多申请了一个O(n)的额外空间。那么有没有空间复杂度为O(1)的的解决方法呢?

法二:递归

法三:改变输入

思路:
反转后半部分链表(修改链表结构),然后用一个固定长度的滑动窗口来比较前半部分和后半部分。
要注意的是,比较完毕我们需要将链表回复原样。(这也算是一个好习惯吧。试想:我们写的只是一个函数,如果在主程序调用函数之后再次使用了该链表了呢?)

小结

【一些提示】

  1. 通过一些测试用例可以节省您的时间。
    使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证您的算法总是很有用的。
  2. 你可以同时使用多个指针。
    有时,当你为链表问题设计算法时,可能需要同时跟踪多个结点。您应该记住需要跟踪哪些结点,并且可以*地使用几个不同的结点指针来同时跟踪这些结点。
    如果你使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。
  3. 在许多情况下,你需要跟踪当前结点的前一个结点。
    你无法追溯单链表中的前一个结点。因此,您不仅要存储当前结点,还要存储前一个结点(前继)。这在双链表中是不同的,我们将在后面的章节中介绍。
相关标签: #leetcode学习