408真题-2019年链表排列

291 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情

Day52 2023/02/26

难度:简单

题目

题目:设线性表L=(a1,a2,a3,......,a(n-2),a(n-1),a(n)),采用带头结点的单链表保存,链表中的结点定义如下。请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到新线性表L'=(a1,a(n),a2,a(n-1),a3,a(n-2),......)
要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
(3)说明你所设计的算法的时间复杂度

示例

输入: 1 2 3 4 5 6 7
输出: 1 7 2 6 3 5 4
说明:按照题目要求改变链表排列之后

思路


观察本题所给改变后的链表排列顺序可以知道,这个排列是从原排列的基础上分别从中心节点和链表的头节点开始一分为二,进行重排列。
具体步骤:

  1. 先找到链表的中心节点,使用双指针cur和center,其中cur每次走两步,center每次走一步,当cur走到表尾的时候,center正好再链表的中心节点。
  2. 从中心节点开始到表尾这段原地翻转,这样链表被分为前后两部分。
  3. 从链表的前后两端依次取一个节点,进行重排。

关键点


  • 初始时:LNode *cur = head->next->next, *center = head;,代表了cur和center已经各自走了一次。
  • while (front != back) ,当 front == back时,说明这时已经重排到中心节点,这最后一个中心节点,总是重排后的最后一个元素,所以不需要再确定其位置。

算法实现


#include <iostream>
using namespace std;

// 定义链表节点
typedef struct LNode {
  int data;                                     // 数据域
  LNode *next;                                  // 指针域
  LNode(int val) : data(val), next(nullptr){};  // 构造函数,初始化链表节点
} LNode, *LinkList;

/**
 * @function 采用尾插法建立单链表 (利用带虚拟头节点的方式)
 * @param num 整型 链表的节点个数
 * @return 结构体 代表一个链表
 */
LinkList ListTailInsert(int num) {
  LNode *dummyHead = new LNode(-1), *r = dummyHead, *s; // 虚拟头节点,不存储数据, 只是为了统一操作; 尾指针; 待插入新节点
  int val;  // 待插入节点的数据
  while (num-- && cin >> val) {
    s = new LNode(val);  // 待插入新节点
    r->next = s;         // 插入操作
    r = s;               // 使r始终指向链表尾元素
  }
  LNode* head = dummyHead->next;  // 链表真正的头节点
  return head;                    // 返回链表真正的头节点 
}

/**
 * @fuction 实现翻转链表元素
 * @param head 结构体 链表头节点
 * @return 结构体 返回反转后链表的第一个节点指针
 */
LinkList ReverseList(LNode *head) {
    LNode *pre = nullptr, *sub = head, *tmp; //pre为前驱节点,sub为后继节点,tmp为临时节点
    while (sub) {
        tmp = sub->next;  // 记录cur的后继节点 
        sub->next = pre;  // 让后继节点指向前驱节点实现链表反转
        pre = sub;        // 更新pre和sub
        sub = tmp;
    }
    return pre;               // 返回反转后链表的第一个节点指针
}

/**
 * @function 按照本题要求重排链表序列
 * @param head 结构体 链表的头节点
 */
void ChangeList(LinkList head) {
    LNode *cur = head->next->next, *center = head;    // 遍历指针,防止污染头节点; 链表中心节点指针
    while (cur && cur->next) {  // 遍历链表,每次center走一步,r走两步,当r走到表位,center正好走到链表中心节点处
        center = center->next;  // center每次走一步
        cur = cur->next->next;  // cur每次走一步
    }
    LNode *back = ReverseList(center->next);  // while循环结束,此时center指向n/2处,需要向下取整,back指向后半段的第一个节点
    LNode *front = head;                      // front指向前半段的第一个节点
    LNode *tmp;                               // tmp临时指针,保存back的后继节点
while (front != back) {                    // front == back时,说明这时已经更新到中心节点,终止循环
        // 前半段链表和后半段链表依次各取一个结点,进行排列
        tmp = back->next;  
        back->next = front->next;
        front->next = back;
        front = back->next;  // 更新front和back
        back = tmp;          
    }
}


//测试一下
int main() { 
    LinkList L = ListTailInsert(7);  // 创建一个大小为7的链表
    ChangeList(L);                       // 重拍链表
    LNode *cur = L;
    while (cur) {
        cout << cur->data << ' ';
        cur = cur->next;
    }
}

c++代码实现

  • 时间复杂度 O(n)O(n)--- 其中原地翻转需要遍历n/2个节点,前后重排的时候一共访问了n个节点,其中n为链表长度
  • 空间复杂度 O(1)O(1)--- 链表为必要空间,其他辅助变量均为常数级,除此无额外的辅助空间

总结

  • 本题代码量比较多,一般考试如果不要求main函数测试的话,可以不写main函数,以及创建链表的函数,代码中实现细节比较多,可以仔细的看看,都有详细的注释。