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

反转链表

如何反转一个单链表?

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

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

因此,时间复杂度为 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;
}

在这里插入图片描述

方法二:迭代

思路:遍历链表时,将当前节点的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;
}

在这里插入图片描述

注意:在写链表的程序的时候,不要对空指针进行比较之类的操作。否则会报错如下:
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;
}

在这里插入图片描述

辅助链表法

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

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

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;
}

在这里插入图片描述

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

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

法二:递归

法三:改变输入

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

小结

【一些提示】

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

猜你喜欢

转载自blog.csdn.net/AuthurWhywat/article/details/105139620