🚀蓝蓝计算机考研算法-day18链表的特殊排列和求三元组中的最小距离

536 阅读8分钟

Day18 2023/3/18

难度:中等

题目1

【2019年统考真题】设线性表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)说明你所设计的算法的时间复杂度

题目2

【2020统考真题】定义三元组(a,b,c)(a、b、c均为正数) 的距离D=|a-b|+|b-c|+|c-a|给定3个非空整数集合S1、S2 和 S3, 按升序分别存储在3个数组中。请设计一个尽可能高效的算法,计算并输出所有可能的三元组(a,b,c) (a∈S1,b∈S2,C∈S3)中的最小距离。例如S1={-1,0,9,} S2={-25,-10,10,11), S3={2,9,17,30,41},则最小距离为2,相应的三元组为(9,10,9)。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C语言或C++语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。

示例

第1题
输入: 1 2 3 4 5 6 7
输出: 1 7 2 6 3 5 4
第2题
输入:S1={-1,0,9,} S2={-25,-10,10,11), S3={2,9,17,30,41}
输出:2 
说明:最小距离为2,相应的三元组为(9,10,9)

运行实例

  1. 第一题
    image.png
  2. 第二题
    image.png

思路1


对于题目1想要解题,一定得仔细观察排序后的链表序列有什么规律,通过观察后我们发现,这个排序后的序列,其实是在原链表的基础上从中心点位置(这里特指以数组下标为顺序的)一分为二得到左右两个链表,再将右链表进行翻转,接着分别从左右链表的头节点出发,依次取一个节点进行重排!(看到这里估计有人还是懵逼😵),这里举个例子:输入:{1 2 3 4} 输出:{1 4 2 3} 具体步骤如下:

  1. 首先一分为二得到,左链表为:{1 2} ,右链表经翻转后为:{4 3}
  2. 从两个链表的头节点出发,依次取一个节点进行重排,首先是1 4 然后是1 4 2 3 其他细节可以对照下面的代码实现,里面有详细的注释!!!

思路2


题目2让我们求三元组中的最小距离,当然可以通过三个for循环实现暴力解法,既然可以通过嵌套循环实现,那一般也可以通过指针实现并降低时间复杂度,这里我们采用三指针的方式实现。假设i j k 分别指向三个数组 a b c,呢我们可以通过一个while循环来计算 |a[i]-b[j]| + |b[j]-c[k]| + |c[k]-a[i]|,关键点是在于每次循环后,该如何更新指针变量。这里详细解释一下:

image.png 如上图:a b c就相当于每次遍历的三个数组元素,三元组的距离自然是ab+bc+ac = 2ac(可以发现距离与点b没有关系)。我们要求的是最小距离,所以我们要考虑的就是如何移动这a c这两个点,使得ab+bc+ac的值最小。

  1. 假设将a向右移动,可以发现ac变小,没问题,但向左移动,则ac变大,不可以
  2. 假设将c向右移动,可以发现ac变大,不可以,但向左移动,则ac变小,可以。
    综上我们可以发现,有两种移动指针的方式,可以使得每次遍历得到的三元组距离变小,其中我们采用第一种方式,将 a 向右移动。(😁够详细了吧!!!)

关键点


  • 对于题目1中遗忘如何 翻转一个链表操作 的可以看这一篇206.翻转链表,加深印象。😐
  • 题目1中获取链表中间位置节点的方法,本质和数组计算中的 除2+1 是一样的,注意这里获取的实际节点的位置是按照数组下标顺序来的。
  • 对于题目1中节点数为偶数时为何要特殊处理一下的原因不太好用语言描述出来,这里大家最好手动模拟一下过程!
  • 题目2 解题思路中a代表的是当前遍历的三个数组元素中的最小值,c为最大值,不要混淆!!!

算法实现


c++代码实现-链表的特殊排列

#include <iostream>
using namespace std;

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

/**
 * @function 采用尾插法建立单链表 (利用带虚拟头节点的方式)
 * @return 指针 代表一个链表
 */
LinkList ListTailInsert() {
  LNode *dummyHead = new LNode(-1), *r = dummyHead, *s;  // 虚拟头节点,不存储数据, 只是为了统一操作; 尾指针; 待插入新节点
  int val;  //待插入节点的数据
  while (cin >> val) {
    s = new LNode(val); // 待插入新节点
    r->next = s;        // 插入操作
    r = s;              // 使r始终指向链表尾元素
    if (cin.get() == '\n') break;  // 最后一个节点元素以回车结束
  }
  LNode *head = dummyHead->next;  // 链表真正的头节点
  delete dummyHead;               // 释放
  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, *center = head;  // 遍历指针 防止污染头节点 链表的中心位置+1的节点指针
  while (cur && cur->next) {          //遍历链表,每次center走一步,r走两步,当cur走到表尾,center正好走到链表中心节点+1处
    cur = cur->next->next;            // cur每次走二步
    center = center->next;            // center每次走一步
  }
  LNode *back = ReverseList(center); // while循环结束,此时center指向n/2+1位置处的节点,back指向后半段的第一个节点
  LNode *front = head;     // front指向前半段的第一个节点
  LNode *tmp;              // tmp临时指针,保存back的后继节点
  while (front != back) {  // 当节点数量为奇数时,front == back时,说明这时已经更新到中心节点,终止循环
    // 前半段链表和后半段链表依次各取一个结点,进行排列
    tmp = back->next;
    back->next = front->next;
    front->next = back;
    if (back == back->next) back->next = nullptr;  // 当节点数量为偶数时需要特殊处理一下。
    front = back->next;                            // 更新front和back
    back = tmp;
  }
}

// //测试一下
int main() {
  LinkList L = ListTailInsert(); // 创建链表
  ChangeList(L);                 // 重排链表
  LNode *cur = L;       
  while (cur) {                  // 打印
    cout << cur->data << ' ';
    cur = cur->next;
  }
}
  • 时间复杂度 O(n)O(n)--- 其中原地翻转需要遍历n/2个节点,前后重排的时候一共访问了n个节点,其中n为链表长度
  • 空间复杂度 O(1)O(1)--- 链表为必要空间,其他辅助变量均为常数级,除此无额外的辅助空间

c++代码实现- 求三元组中的最小距离

#include <iostream>
#include <vector>
using namespace std;

// 求三数中的最小值
int MinNum(int a, int b, int c) {
  int min = a < b ? a : b;
  min = min < c ? min : c;
  return min;
}

// 按题目要求,求三元组的最小距离
int MinDistance(vector<int> a, vector<int> b, vector<int> c) {
  int minDis = 100000;      // 初始值为无穷大,表示三元组中的最小距离
  int curDis = 0;           // 表示当前三元组距离
  int min = 0;              // 当前遍历的三个数中的最小值
  int i = 0, j = 0, k = 0;  // 三个整型数组的指针
  while (i < a.size() && j < b.size() && k < c.size()) {
    // 优化循环,其中a b c均为正数
    if(a[i] <= 0) {
      i++;
      continue;
    }else if(b[j] <= 0) {
      j++;
      continue;
    }else if(c[k] <= 0) {
      k++;
      continue;
    }
    curDis = abs(a[i] - b[j]) + abs(b[j] - c[k] + abs(c[k] - a[i]));  // 计算当前三元组距离 
    if (curDis < minDis) minDis = curDis;                             // 让minDis等于当前三元组的最小距离 
    min = MinNum(a[i], b[j], c[k]);                                   // 计算三数中的最小值
    // 让当前最小的那一指针向前移动一位
    if (min == a[i]) i++;
    else if (min == b[j]) j++;
    else if (min == c[k]) k++;
  }
  return minDis;    // 返回结果
}

// 测试一下
int main() {
    // 测试数据
    vector <int> a = {-1, 0, 9};
    vector <int> b = {-25, -10, 10, 11};
    vector <int> c = {2, 9, 17, 30, 41};
    int res = MinDistance(a, b, c);  // 调用
    cout << "三元组中最小距离为:" << res;
}
  • 时间复杂度 O(n+m+v)O(n+m+v)--- 最坏情况下,访问了三个数组中的全部元素,其中n ,m, v分别为三个数组的长度
  • 空间复杂度 O(1)O(1)--- 数组为必要空间,其他辅助变量均为常数级,除此无额外的辅助空间

总结

  • 今天两题难度中等,算是有难度的,我发现真题中有难度的题目,一般实现代码中,都涉及两个及其以上的的问题,例如第1题中想要实现重排,就涉及到翻转链表的操作,还有第2题 中的求最小数,也仅仅是题解中的一小部分,随着难度的增加,题目越来越综合,虽然一开始做起来会有些头疼,但是实际上只是把我们之前学过的单个操作整合起来,通过2到3个操作综合起来去解题🥳。