【数据结构练习题】反转链表

1. 反转链表

题目来源:反转链表

题目的链表结构说明:其头节点是有值的,可以理解为头结点就是首元节点,如下图:

链表说明

链表反转的思路如下:

prior指向当前节点的上一个节点

head指向当前节点

next指向当前节点的下一个节点

反转链表

其中prior指针的初始值一定为NULL!因为第1次反转的节点成为了最后1个节点,最后1个节点的指针域为NULL

//链表节点包含的内容
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

struct ListNode* ReverseList(struct ListNode* head ) {

    struct ListNode *next = NULL;  //记忆下一个节点的位置,初值任意
    struct ListNode *prior = NULL; //记忆前一个节点的位置,初值必为NULL

    while (head!=NULL) //链表是否结束
    {
        next = head->next;  //记忆下一个节点位置
        head->next = prior; //反转
        prior = head;       //记忆前一个节点位置
        head = next;        //移到到下一个节点
    }

    return prior; //prior此时指向反转前链表的最后一个节点
}

2. 链表内指定区间反转

题目来源:链表内指定区间反转

2.1 基本解法

解题思路:

  1. 反转局部链表,可以将区间的部分当作完整链表进行反转
  2. 再将已经反转好的局部链表与其他节点建立连接,重构链表
  3. 使用虚拟头节点的技巧,可以避免对头节点复杂的分类考虑,简化操作

start指向翻转链表区间外部前一个节点

end指向翻转链表区间外部后一个节点

left指向翻转链表区间内部的第一个节点

right指向翻转链表区间内部的最后一个节点

注:不太清楚的可看下面的示意图,红框框出来的部分表示要反转的区间。

prior指向当前节点的上一个节点

head指向当前节点

next指向当前节点的下一个节点

上面这3个指针在下面的示意图没有体现出来,因为在上面的反转链表部分已经讲过了。

区间链表反转

//链表节点包含的内容
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    
    struct ListNode phead; //创建虚拟的头结点
    phead.next = head; //虚拟头结点指向链表的头结点

    struct ListNode *next = NULL;  //反转的下一个节点
    struct ListNode *prior = NULL; //反转的前一个节点,初始值一定为NULL

    struct ListNode *start = &phead; //反转链表区间外部的前一个节点
    struct ListNode *end = NULL;    //反转链表区间外部的后一个节点

    struct ListNode *left = NULL;   //反转链表区间内部的第一个节点
    struct ListNode *right = NULL;  //反转链表区间内部的最后一个节点

    int i = 0;

    for (i=0; i<m-1; ++i) //找到链表反转的前一个节点
    {
        start = start->next; //start的起始值:并不是整个链表的第一个节点
    }

    head = right = left = start->next; //链表反转的第一个节点

    for (i=m; i<n; ++i) //找到链表反转的最后一个节点
    {
        right = right->next;
    }

    end = right->next; //反转链表结束后的节点

    right->next = NULL; //将反转部分断开,不然下面的判断条件head!=NULL会出现问题

    while (head!=NULL)
    {
        next = head->next;  //存储下一个节点
        head->next = prior; //反转
        prior = head;       //存储前一个节点
        head = next;        //移到下一个节点
    }

    start->next = right; //将断开的链表接上
    left->next = end;    //将断开的链表接上

    return phead.next;
}

创建虚拟头节点的错误方法:

struct ListNode *phead = (struct ListNode*)malloc(sizeof(struct ListNode)); //虚拟头结点
phead->next = head;

按照上述方法创建,需要释放掉malloc在堆上开辟的空间,函数内部无法通过free函数释放。因为该函数返回的是开辟节点的指针域,返回前如果释放掉开辟节点,会导致函数返回失败。

如果在函数外部释放,因为返回的是开辟节点的指针域,即链表的头结点的地址,没有返回开辟节点的地址,找不到虚拟头节点的地址,因此函数外部无法释放。

创建虚拟头节点时应该创建局部变量,这样函数结束后会自动销毁:

struct ListNode phead; //创建虚拟的头结点
phead.next = head; //虚拟头结点指向链表的头结点

注意:start指针的初始值一定为虚拟的头节点,因为可能从链表的第1个节点就开始反转。start指向的是反转区间外部的前一个节点。

这里prior的起始值可以不为NULL,因为后面的代码left->next = end;会起到相同的作用。

2.2 进阶解法 - 头插法简易版(推荐)

因为在 m 位置到 n 位置区间进行反转,故在 m-1 结点后进行头插法,依次将翻转区间内的结点插入到 m-1 结点后。
在这里插入图片描述

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    
    //定义虚拟头结点, 以防从第1个结点就开始翻转
    struct ListNode Head;
    Head.next = head;

    struct ListNode* pHead = &Head; //pHead指向第 m-1 个结点
    struct ListNode* next = NULL;
    struct ListNode* first = NULL;

    // 找到第 m-1 个结点
    for (int i=1; i<m; ++i)
    {
        pHead = pHead->next;
    }
    
	// 记录第 1 个翻转结点的位置
    first = head = pHead->next;

    // m~n 个结点进行反转
    for (int i=m; i<=n; ++i)
    {
        next = head->next;
        head->next = pHead->next;
        pHead->next = head;
        head = next;
    }
	
    // 连上断开的链表
    first->next = head;
    return Head.next;
}

2.3 进阶解法 - 头插法

在上一种方法中,需要先找到end指针要指向的结点,再重新进行翻转,需要遍历2次。

头插法法只需要遍历1次即可,一边遍历一边翻转。

pstart指向翻转链表区间外部前一个节点

head指向翻转的当前节点

next指向翻转的下一个节点

核心:将next指针指向的节点作为插入的节点,接在pstart指针指向的节点后面。

下图为区间2-4的翻转示意图:

反转链表_2

翻转时顺序:

  1. head指向结点的指针域设置为next指向结点的下一个结点
  2. next指向结点的指针域设置为pstart指向结点的下一个结点
  3. pstart指向结点的指针域设置为next指向结点
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    struct ListNode phead; //创建虚拟的头结点
    phead.next = head; //虚拟头结点指向链表的头结点
    struct ListNode* pstart = &phead; //要指向翻转区间的前一个结点
    struct ListNode* next = NULL;
    int i = 0;

    //从虚拟头结点开始寻找,以防第1个结点属于翻转区间
    for (i=0; i<m-1; ++i)
    {
        pstart = pstart->next;
    }

    head = pstart->next; //第1个要翻转的结点

    //循环体中的顺序不能更换!
    for (i=m; i<n; ++i) 
    {   
        next = head->next;
        head->next = next->next;
        next->next = pstart->next;
        pstart->next = next;
    }

    return phead.next;
}

3. 链表中的节点每k个一组翻转

题目来源:链表中的节点每k个一组翻转

解题思路:

  1. 先求出链表长度lengthlength/k为整个链表要反转的次数
  2. 区间的部分当作完整链表进行反转
  3. 再将已经反转好的局部链表与其他节点建立连接,重构链表
  4. 在下一次反转前,要先设置好对应的指针

第2, 3步的处理上面已经解决过了,主要是剩下的第1步和第4步该如何处理

下面代码中的指针功能同第2题:链表内指定区间反转。

下图主要列出了一些主要的指针,其他不重要的指针可由主要的指针所推出来。

节点每k个一组反转

上图中prior指针一开始指向哪不重要,针对每次翻转的第1个节点,后面的代码left->next = end;会起到相同的作用。

每次翻转的第1个节点会被left指针所指;left->next = end;会连接上断开的链表。

实际翻转过程会在第2步和第3步不断循环。

所以每次翻转后,指针的设置非常重要!

struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
    
    struct ListNode pHead; //创建虚拟的头结点
    pHead.next = head; //虚拟头结点指向链表的头结点
    
    struct ListNode* start = &pHead; //初值只能是这个,不可更改
    struct ListNode* end = NULL;

    struct ListNode* prior = NULL; 
    struct ListNode* next = &pHead; 

    struct ListNode* left = head; //初值只能是这个,不可更改
    struct ListNode* right = &pHead; //初值只能是这个,不可更改

    int i = 0;
    int j = 0;
    int length = 0; //链表长度

    //求链表长度。不创建新的指针,借助next指针来求取长度
    for (length=0; next->next!=NULL; ++length)
    {
        next = next->next;
    }
        
    //链表的反转次数为length/k -> 为整数
    for (i=0; i<(length/k); ++i)
    {
        //------------------以下代码同第2题:链表内指定区间反转
        for (j=0; j<k; ++j)
        {
            right = right->next;
        }

        end = right->next;
        right->next = NULL;
        
        while(head!=NULL)
        {
            next = head->next;
            head->next = prior;
            prior = head;
            head = next;
        }

        start->next = right;
        left->next = end;
		//------------------以上代码同第2题:链表内指定区间反转
        	
        //在下一次反转前预先设置好对应的指针
        start = right = left;       
        head = left = left->next; //下一次反转的第1个节点位置
    }

    return pHead.next;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基础数据结构是Java编程中非常重要的一部分,练习题可以帮助我们巩固对这些数据结构的理解和使用。下面是一些常见的Java基础数据结构练习题: 1. 数组:编写一个方法,将给定的数组按照逆序进行排列。 2. 查找元素:编写一个方法,在给定的有序整数数组中查找指定元素,返回其索引;如果不存在,则返回-1。 3. 字符串:编写一个方法,将给定的字符串按照逆序进行排列。 4. 链表逆序:编写一个方法,将给定的单链表进行逆序排列。 5. 栈的应用:使用栈来检查给定的括号序列是否合法,例如{[()]}是合法的,而{[(])}是非法的。 6. 队列的应用:使用队列来实现热土豆游戏,每经过指定的时间,队列中的土豆将被传递给下一个人,最后队列中剩下的人即为胜者。 7. 哈希表应用:实现一个电话号码簿,可以添加、删除和查找联系人信息。 8. 树的遍历:实现二叉树的前序、中序和后序遍历算法。 9. 图的最短路径:使用Dijkstra算法求解给定图中两个节点的最短路径。 10. 排序算法:实现常见的排序算法,如冒泡排序、插入排序和快速排序。 通过这些练习题的学习和实践,我们可以巩固对基础数据结构的理解和运用,提高自己的编程能力。同时,这些题目也是我们面试和应聘工作中常见的考察点,掌握这些知识也有助于我们在求职过程中脱颖而出。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值