24、将有序线性表进行头尾交叉排列
【2019年统考真题】设线性表L=(a1,a2,a3,...,an-2,an-1,an)采用带头结点的单链表保存,链表中的节点定义如下:
typedef struct node {
int data;
struct node* next;
}
请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列题中的各个结点,得到线性表L=(a1,an,a2,an-1,a3,an-2,...)
要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所涉及的算法的时间复杂度。
示例:
输入: 1 2 3 4 5 6 7
输出: 1 7 2 6 3 5 4
思路
这个排序后的序列,其实是在原链表的基础上从中心点位置(这里特指以数组下标为顺序的)一分为二得到左右两个链表,再将右链表进行翻转,接着分别从左右链表的头节点出发,依次取一个节点进行重排!
这里举个例子:输入:{1 2 3 4} 输出:{1 4 2 3} 具体步骤如下:
- 首先一分为二得到,左链表为:{1 2} ,右链表经翻转后为:{4 3}
- 从两个链表的头节点出发,依次取一个节点进行重排,首先是1 4 然后是1 4 2 3 其他细节可以对照下面的代码实现,里面有详细的注释!
- 获取链表中间位置节点的方法,本质和数组计算中的 除2+1 是一样的,注意这里获取的实际节点的位置是按照数组下标顺序来的。
具体实现
#include<stdlib.h>
#include<stdio.h>
// 定义链表节点
typedef struct LNode {
int data; // 数据域
LNode* next; // 指针域
// LNode(int val) : data(val), next(nullptr) {}; // 构造函数,初始化链表节点
} LNode, * LinkList;
//初始化链表-带头结点
bool InitLinkList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode));
if (L == NULL) {
return false;//内存分配失败
}
L->next = NULL;//头结点后暂时没有结点
return true;
}
//尾插法创建单链表-有头结点
LinkList ListTailInsert(LinkList &L) {
int val; //设元素类型为整型
L = (LinkList)malloc(sizeof(LNode));//建立头结点
LNode* s, * r = L; //r是尾指针
while (scanf_s("%d",&val))
{
s = (LNode*)malloc(sizeof(LNode));// 待插入新节点
s->data = val;
r->next = s; // 插入操作
r = s; // 使r始终指向链表尾
if (getchar() == '\n')
break; // 最后一个节点元素以回车结束
}
//LNode* head = L->next; // 链表真正的头节点
//delete L; // 释放
//return head; // 返回链表真正的头节点
r->next = NULL;//尾结点指针置空
return L; // 返回链表头节点
}
//实现翻转链表
LinkList ReverseList(LNode* L) {
LNode* pre = NULL, * sub = L, * tmp; // pre为前驱节点,sub为后继节点,tmp为临时节点
while (sub) {
tmp = sub->next; // 记录cur的后继节点
sub->next = pre; // 让后继节点指向前驱节点实现链表反转
pre = sub; // 更新pre和sub
sub = tmp;
}
return pre; // 返回反转后链表的第一个节点指针
}
//重排链表序列
void ChangeList(LinkList head) {
LNode* cur = head, * center = head; // 遍历指针 防止污染头节点 链表的中心位置+1的节点指针
while (cur && cur->next) { //遍历链表,每次center走一步,cur走两步,当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 = NULL; // 当节点数量为偶数时需要特殊处理一下。
}
front = back->next; // 更新front和back
back = tmp;
}
}
// //测试一下
int main() {
LinkList L; //声明一个指向单链表的指针
// InitLinkList(L);//初始化一个空表
ListTailInsert(L); // 尾插法创建链表
ChangeList(L); // 重排链表
LNode* cur = L->next;
while (cur !=NULL ) { // 打印
printf("%d ", cur->data);
cur = cur->next;
}
}
运行结果
与题目相反了!!!!!
空间复杂度
- 时间复杂度 O(n)--- 其中原地翻转需要遍历n/2个节点,前后重排的时候一共访问了n个节点,其中n为链表长度
- 空间复杂度O(1)--- 链表为必要空间,其他辅助变量均为常数级,除此无额外的辅助空间
另一种思路写法
将翻转与排列写在一起的
// 链表翻转-重排列函数
struct LNode* ChangeList(struct LNode* head) {
LNode* p, * q, * r, * s;
p = q = head;
while (q->next != NULL) { // 寻找中间节点
p = p->next; // p走一步
q = q->next;
if (q->next != NULL)
q = q->next; // q走两步
}
q = p->next; // p所指节点为中间节点,q为后半段链表的首节点
p->next = NULL;
while (q != NULL) { // 将链表后半段逆置
r = q->next;
q->next = p->next;
p->next = q;
q = r;
}
s = head; // s变成头节点
q = p->next; // q是后半段的第一个数据点
p->next = NULL;
while (q != NULL) { // 将链表后半段插入指定位置
r = q->next;
q->next = s->next;
s->next = q;
s = q->next;
q = r;
}
return head;
}
#define _CRT_SECURE_NO_WARNINGS //防报错
25、求元素间的距离。
【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)说明你所涉及的算法的时间复杂度。
示例
输入:S1={-1,0,9,} S2={-25,-10,10,11),
S3={2,9,17,30,41}
输出:2 说明:最小距离为2,相应的三元组为(9,10,9)
思路
让我们求三元组中的最小距离,当然可以通过三个for循环实现暴力解法,既然可以通过嵌套循环实现,那一般也可以通过指针实现并降低时间复杂度,这里我们采用三指针的方式实现。假设i j k 分别指向三个数组 a b c,呢我们可以通过一个while循环来计算 |a[i]-b[j]| + |b[j]-c[k]| + |c[k]-a[i]|
,关键点是在于每次循环后,该如何更新指针变量。这里详细解释一下:
如上图:a b c就相当于每次遍历的三个数组元素,三元组的距离自然是ab+bc+ac = 2ac(可以发现距离与点b没有关系)。我们要求的是最小距离,所以我们要考虑的就是如何移动这a c这两个点,使得ab+bc+ac的值最小。
- 假设将a向右移动,可以发现ac变小,没问题,但向左移动,则ac变大,不可以
- 假设将c向右移动,可以发现ac变大,不可以,但向左移动,则ac变小,可以。
综上我们可以发现,有两种移动指针的方式,可以使得每次遍历得到的三元组距离变小,其中我们采用第一种方式,将 a 向右移动。
注意:解题思路中a代表的是当前遍历的三个数组元素中的最小值,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;
}
实现二
由数学公式可推知,两个数距离的和其实就是最小值与最大值之间的距离的2倍,所以我们只需要固定一个值,假定为c,寻找与其最接近的a、b的值,将最大的和最小的相减即可得到最小的距离。
#include <stdio.h>
#include <stdlib.h>
#define INT_MAX 0x7fffffff
// 计算绝对值
int absNum(int a) {
if (a < 0)
return -a;
else
return a;
}
// a是否是三个数中的最小值
bool minNum(int a, int b, int c) {
if (a < -b && a <= c)
return true;
return false;
}
// 计算三元组的最小距离
int findMinofTrip(int A[], int n, int B[], int m, int C[], int p) {
int i = 0, j = 0, k = 0, min = INT_MAX, d;
while (i < n && j < m && k < p && min > 0) {
// 计算d
d = absNum(A[i] - B[j]) + absNum(B[j] - C[k]) + absNum(C[k] - A[i]);
if (d < min)
min = d; //更新 d
if (minNum(A[i], B[j], C[k]))
i++;
else if (minNum(B[j], C[k], A[i]))
j++;
else
k++;
}
return min;
}
int main() {
int a[]={ -1, 0, 9 } , b[] = { -25, -10, 10, 11 }, c[] = { 2, 9, 17, 30, 41 };//测试数据
int d = findMinofTrip(a, 3, b, 4, c, 5);
printf("最小距离是%d", d);
return 0;
}
运行结果
复杂度
- 时间复杂度 O(n+m+v)--- 最坏情况下,访问了三个数组中的全部元素,其中n ,m, v分别为三个数组的长度
- 空间复杂度 O(1)--- 数组为必要空间,其他辅助变量均为常数级,除此无额外的辅助空间