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

52 阅读8分钟

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

运行结果

与题目相反了!!!!!

image.png

空间复杂度

  • 时间复杂度 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]|,关键点是在于每次循环后,该如何更新指针变量。这里详细解释一下:

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

  1. 假设将a向右移动,可以发现ac变小,没问题,但向左移动,则ac变大,不可以
  2. 假设将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;
}

运行结果

image.png

复杂度

  • 时间复杂度 O(n+m+v)--- 最坏情况下,访问了三个数组中的全部元素,其中n ,m, v分别为三个数组的长度
  • 空间复杂度 O(1)--- 数组为必要空间,其他辅助变量均为常数级,除此无额外的辅助空间