2009 单链表(双指针)
分析:首先呢,给我们的数据结构是一个带有表头结点的单链表,也不允许我们改变链表的结构。链表的长度不是直接给出的啊,所以这个倒数也很棘手。那我们该如何解决这个“k”呢,我们并不能一下子就知道这个倒数第k位置在哪里,不过不妨倒着想一下,如果现在有一个指针指向尾结点,又有一个指针指向倒数第k个。那我们再逆推一下过程这两个指针一起往回走,当先前指向倒数第k个结点的指针走到表头与list相会时,后面那个指针是不是也到正数第k个结点的头上了?那是不是我们的问题就解决了。
思路:设置两个指针p、q,初始时都指向list,让q先往后走k步(计数器实现),这时再让p、q同时朝后走,直至q到达尾指针(所指的next为空),那么此时此刻的p所指向的结点既是我们所需要的倒数第k个结点,将其data值输出。
详细实现步骤:
- 初始化指针: 设置两个指针
p
和q
,初始时都指向链表的头节点list
。 - 移动快指针: 让指针
q
向前移动k步。这里需要注意的是,如果k
大于链表的长度,那么查找失败,因为不存在倒数第k个节点。 - 同步移动双指针: 当
q
成功移动了k步并且q
不为空(即没有到达链表末尾)时,开始同步移动p
和q
两个指针。每次循环,p
和q
都同时向后移动一步,即p = p->next;
和q = q->next;
。 - 查找结束条件: 当指针
q
到达链表的末尾(即q->next
为NULL
)时,停止移动。此时,p
所指向的节点就是链表中倒数第k个节点。 - 检查并输出结果: 检查指针
p
是否为NULL
。如果p
不为空,说明找到了倒数第k个节点,输出该节点的data
域的值,并返回1表示成功。如果p
为空,说明没有找到倒数第k个节点,返回0表示失败。 - 返回结果: 函数返回查找的结果,即1或0。
注:有可能参考答案里头并不是这样写的呀,我还没搞懂这个评分细则。(大概是这样)
// 假设LinkList是如下定义的结构体
typedef struct LinkNode {
int data;
struct LinkNode *next;
} LinkList;
int findKthFromEnd(LinkList list, int k) {
LinkList p, q;
p = list;
q = list;
int cnt = 0;
// 让q先向后走k步
while (cnt < k && q != NULL) {
q = q->next;
cnt++;
}
// 当q到达第k+1个节点或链表尾部时,p和q一起向后移动
while (q != NULL) {
p = p->next;
q = q->next;
}
// 此时p指向的就是倒数第k个节点,如果存在的话
if (p != NULL) {
printf("%d", p->data);
return 1; // 查找成功
} else {
return 0; // 查找失败
}
}
2010 顺序表
分析:
“循环”左移,那这个循环就应该是我们需要重点思考的点。先考虑最简单的我们可以设置两个数组,其中一个数组保存的是原数据,另一个初始为空。接着想要实现循环左移就只需要找出相对应的位置再一一赋值即可。(我比较笨只会右移,就把左移模拟成右移了)
到这会儿又可以开始想一下如何在时间和空间上优化了。空间的话,我们是可以只用一个数组(+一个中间变量+循环遍历)来实现的。原本我觉得已经可以了啊,然后看了书上觉得比较秒也更好实现。在时间上呢,也是能够发现一个规律。
1、算法思想
- 反转数组:首先反转整个数组 R。
- 反转前 n−p 个元素:接着反转数组的前 n−p 个元素。
- 反转后 p 个元素:最后反转数组的后 p 个元素。
2、算法实现
// 反转数组的辅助函数
void reverse(int arr[], int start, int end) {
while (start < end) {
// 交换元素
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
start++;
end--;
}
}
// 循环左移 p 个位置
void rotate(int arr[], int n, int p) {
// 反转整个数组
reverse(arr, 0, n - 1);
// 反转前 n - p 个元素
reverse(arr, 0, n - p - 1);
// 反转后 p 个元素
reverse(arr, n - p, n - 1);
}
3、复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 常数空间使用:该算法仅使用了几个额外的变量(如
temp
、start
和end
),这些变量的数量与输入数组的大小无关。 - 不使用额外数组:算法在进行反转时,直接在原数组上进行操作,而不需要创建新的数组来存放中间结果。
- 常数空间使用:该算法仅使用了几个额外的变量(如
2011 顺序表
分析:
先不考虑细节问题,最简单的做法当然是先将两个数组合并啦然后直接找中位数。
但是我们仔细想想我们只需要找出来并不需要实际的再花额外空间去合并,故只需设置一个计数器和两个指针从两个数组头开始遍历比大小,小的放行,大的留着下一趟比,然后再将计数器加1,加到一个数组的长度时即是我们所求的答案。那这样的话,时间复杂度就是O(n),空间复杂度就是O(1) 。
int findMedian(int A[], int B[], int n) { //n是一个升序序列的长度
int i = 0, j = 0;
int cnt = 0;
int temp = 0;
while (cnt < n) {
if (i < n && (j >= n || A[i] <= B[j])) {
temp = A[i++];
} else {
temp = B[j++];
}
cnt++;
}
return temp;
}
书里头给了这种就是二分咯,没有往这个方面想,那时间复杂度就会缩减到O(log2n),空间复杂度就还是一样的。
#include <stdio.h>
int findMedian(int A[], int B[], int n) {
int s1 = 0, d1 = n - 1;
int s2 = 0, d2 = n - 1;
while (s1 < d1 || s2 < d2) {
int ml = (s1 + d1) / 2;
int m2 = (s2 + d2) / 2;
if (A[ml] == B[m2]) {
return A[ml]; // 找到中位数
}
if (A[ml] < B[m2]) {
if ((d1 - s1 + 1) % 2 == 1) { // 奇数个元素
s1 = ml; // 保留中间点
} else { // 偶数个元素
s1 = ml + 1; // 舍弃前半部分
}
d2 = m2; // 舍弃 B 的后半部分
} else {
if ((d2 - s2 + 1) % 2 == 1) { // 奇数个元素
d1 = ml; // 舍弃 A 的后半部分
} else { // 偶数个元素
d1 = ml - 1; // 保留中间点
}
s2 = m2 + 1; // 舍弃 B 的前半部分
}
}
// 返回中位数
if (s1 > d1) return B[s2]; // A 完全被舍弃
if (s2 > d2) return A[s1]; // B 完全被舍弃
return (A[s1] < B[s2]) ? A[s1] : B[s2]; // 返回较小的
}
// 测试函数
int main() {
int A[] = {1, 3, 8, 9, 15};
int B[] = {7, 11, 18, 19, 21};
int n = sizeof(A) / sizeof(A[0]);
int median = findMedian(A, B, n);
printf("中位数是: %d\n", median);
return 0;
}
2012 单链表(双指针)
分析:最简单的应该是,直接安排两个指针遍历两个链表,再将其数据域的值赋给两个辅助数组,然后根据数组可以比较简单的先把共同后缀比出来,再利用两个index值来判断除去共同后缀之后的长度为多少,再让指针遍历一遍链表,到相对应的位置之后让其中一个指向另一个所指向的结点。(不考虑释放另一边所占用的空间)
(书里那个方法我想不到啊 q"o"q)
算法的基本设计思想:
- 确定较长链表: 首先确定两个链表中较长的一个,因为共同后缀不可能长于较短的链表。
- 移动较短链表的指针: 将较短链表的指针移动到与较长链表相同长度的位置。
- 同时遍历: 从这个点开始,同时遍历两个链表,比较每个节点的值。
- 找到共同后缀: 当两个指针同时到达链表末尾或者发现不匹配的节点时,停止遍历。如果到达末尾,说明找到了共同后缀;否则,记录下不匹配时两个链表的指针位置。
算法的详细实现步骤:
- 计算两个链表的长度。
- 使用较长链表的长度减去较短链表的长度,得到需要前进的步数。
- 将较长链表的头指针移动到倒数第k个节点,其中k为需要前进的步数。
- 同时遍历两个链表,直到两个指针都到达末尾或者发现不匹配的节点。
- 如果两个指针都到达末尾,返回较长链表的当前节点,即为共同后缀的起始位置。
- 如果发现不匹配的节点,返回
nullptr
,表示没有共同后缀。
// 定义链表节点结构
struct ListNode {
int data;
ListNode* next;
ListNode(int val) : data(val), next(nullptr) {}
};
// 计算链表长度
int getLength(ListNode* head) {
int length = 0;
ListNode* temp = head;
while (temp != nullptr) {
length++;
temp = temp->next;
}
return length;
}
// 找到共同后缀的起始位置
ListNode* findCommonSuffix(ListNode* str1, ListNode* str2) {
int len1 = getLength(str1);
int len2 = getLength(str2);
// 确定较长的链表并移动指针
ListNode* longer = (len1 > len2) ? str1 : str2;
ListNode* shorter = (len1 > len2) ? str2 : str1;
// 计算步数并移动较长链表的指针
int steps = std::abs(len1 - len2);
for (int i = 0; i < steps; ++i) {
if (longer == nullptr) return nullptr; // 防止链表为空
longer = longer->next;
}
// 同时遍历两个链表
while (longer != nullptr && shorter != nullptr) {
if (longer->data != shorter->data) {
return nullptr; // 如果发现不匹配的节点,返回nullptr
}
longer = longer->next;
shorter = shorter->next;
}
// 如果遍历结束于链表末尾,返回共同后缀的起始位置
if (longer == nullptr && shorter == nullptr) {
return nullptr; // 两个链表都到达末尾,表示没有共同后缀
}
// 如果 shorter 先到达末尾,返回 longer 的当前节点
return longer; // 此时 longer 指向共同后缀的起始位置
}
2013 顺序表
分析:
最简单的做法应该可能大概是直接rua个大数组(n范围没讲,就只能尽量大咯)然后对这个整数数列进行扫描,将其值映射到大数组中的下标,然后大数组该对应下标的元素值加1(也就是遍历一遍整数数列,用大数组统计这个整数数列的每个数都出现了多少次)最后如果大数组中元素最大的值未能大于长度的一般则输出-1。
int MajorityElement(int A[], int n) {
// 创建大小为 n 的统计数组并初始化为 0
int *count = (int *)calloc(n, sizeof(int));
// 遍历整数序列,统计每个数的出现次数
for (int i = 0; i < n; i++) {
count[A[i]]++;
}
// 查找出现次数最多的元素
int majorityElement = -1;
int maxCount = 0;
for (int i = 0; i < n; i++) {
if (count[i] > maxCount) {
maxCount = count[i];
majorityElement = i;
}
}
// 检查是否为主元素
if (maxCount > n / 2) {
free(count); // 释放内存
return majorityElement;
} else {
free(count); // 释放内存
return -1; // 不存在主元素
}
}
书里这个应该是一种特殊的算法蛤,没必要背,了解即可。
int fMajorityElement(int A[], int n) {
int candidate = -1; // 候选元素
int count = 0; // 计数器
// 第一步:选取候选元素
for (int i = 0; i < n; i++) {
if (count == 0) {
candidate = A[i]; // 更新候选元素
count = 1; // 重置计数器
} else if (A[i] == candidate) {
count++; // 候选元素的支持度加一
} else {
count--; // 其他元素的支持度减一
}
}
// 第二步:验证候选元素
count = 0; // 重置计数器
for (int i = 0; i < n; i++) {
if (A[i] == candidate) {
count++; // 统计候选元素出现的次数
}
}
// 检查候选元素是否为主元素
if (count > n / 2) {
return candidate; // 返回主元素
} else {
return -1; // 不存在主元素
}
}
原理来自 Boyer-Moore 投票算法,它是一种高效的寻找主元素的算法。
-
候选元素的选取:
- 使用一个计数器来记录当前候选元素的“支持度”。
- 遍历数组时,如果计数器为零,选择当前元素作为新的候选元素,并将计数器设为1。
- 如果当前元素与候选元素相同,增加计数器;如果不同,减少计数器。
-
为什么有效:
- 如果存在一个元素的出现次数超过 n/2,那么在遍历过程中,该元素将始终保持为候选元素。
- 计数器的增加和减少保证了在元素数量相等时,候选元素的优势可以保持。
具体步骤:
-
初始化:
- 设置一个候选元素
candidate
和一个计数器count
,初始值为 0。
- 设置一个候选元素
-
遍历数组:
- 对于每个元素:
- 如果
count
为 0,更新candidate
为当前元素,并将count
设置为 1。 - 如果当前元素等于
candidate
,则count
加 1。 - 如果当前元素不等于
candidate
,则count
减 1。
- 如果
- 对于每个元素:
-
验证候选元素:
- 遍历数组,统计
candidate
的出现次数,确认是否超过 n/2。
- 遍历数组,统计
2014 二叉树(WPL)
1. 算法基本设计思想
带权路径长度(WPL)是二叉树中所有叶结点的权值与其到根节点的路径长度的乘积之和。为了计算 WPL,可以采用深度优先遍历(DFS)的方法,遍历树的每一个节点:
- 对于每个节点,记录当前深度。
- 当到达叶节点时,计算其带权路径长度 WPL+=weight×depth WPL+=weight×depth
2. 二叉树节点的数据类型定义
typedef struct BiNode {
struct BiNode* left; // 指向左子树的指针
struct BiNode* right; // 指向右子树的指针
int weight; // 节点的权值(仅在叶节点有效)
};
3. WPL 计算的 C++ 实现
以下是计算二叉树 WPL 的算法实现:
//Way1:先序遍历
int ans=0;
void func(BiTree root,int depth){
if(root==NULL) return;
if(root->left==NULL&&root->right==NULL){
ans+=root->weight*depth;
}
func(root->left,depth+1);
func(root->right,depth+1);
}
int WPL(Bitree root){
func(root,0);
return ans;
}
- 时间复杂度:O(n),其中 nn 是树中节点的数量,因为每个节点都被访问一次。
- 空间复杂度:O(h),其中 hh 是树的高度,递归调用栈的深度。
这样就完成了 WPL 的计算算法设计与实现!
//Way2:层次遍历
int func(BiTree root, int depth) {
queue<BiTNode*> q; // 创建队列用于层次遍历
int ans = 0, depth = 0; // 初始化结果ans和当前深度depth
if (root != NULL) q.push(root); // 如果根节点不为空,则入队
while (!q.empty()) { // 当队列不为空时循环
int n = q.size(); // 获取当前队列的大小,即当前层的节点数
for (int i = 0; i < n; i++) { // 遍历当前层的所有节点
BiTNode* node = q.front(); // 获取队列前端的节点
q.pop(); // 弹出队列前端的节点
if (node->left != NULL) q.push(node->left); // 如果左孩子不为空,则入队
if (node->right != NULL) q.push(node->right); // 如果右孩子不为空,则入队
// 如果当前节点为叶子节点(没有左右孩子)
if (node->left == NULL && node->right == NULL) {
ans += node->weight * depth; // 累加叶子节点的权重乘以当前深度
}
}
depth++; // 每遍历完一层,深度增加
}
return ans; // 返回最终计算的结果
}
2015 单链表(双指针)
分析:首先,依然还是单链表所以其上的指针只能一条路走到黑,那我们其实可以设置两个指针一个走慢一点,走在快指针的后一步。那这样是不是就可以实现找前驱的工作了。然后我们可以再设置一个遍历指针,在快慢指针行进过程中的每一步都从慢指针开始向后遍历。在找到绝对值相同的结点时,利用快慢指针实现删除操作(快指针走一步,慢指针直接指向快指针)。这样实现起来很简单对吧,但是时间复杂度有点点高,需要O(n^2)
不对不对是保留第一次出现的,那就固定一个指针用于遍历,该指针每遍历一个结点就发出一对快慢指针用于删除后续绝对值重复的结点。但是时间复杂度也依旧是O(n^2)。
那如果我先遍历一遍单链表将其数据域的值都用一个cnt记录出现次数(初始为-1,出现1次自动加1)然后最后再用两个指针进行遍历,遍历的同时比对所存放的cnt值是否大于0(重复程度)。所有数据第一次遇到均不会删除只会cnt-1,若后续重复遇到即可根据cnt的值来判断之后要删除几个结点(也同样是用快慢指针来实现删除结点的操作),这时候时间复杂度应该为O(n^2)。
不对不对,我们可以一趟就完成这个事情,直接边遍历边记录。只需要利用数组在遍历的同时记录下链表中已经出现的数值,后续都先过一遍数组(但是又因为数组是可以随机存储的所以不影响)看看该值是否出现过,再碰到直接一删除不就完了。
(书里给的也是这样蛤,但是是直接申请空间来表示)
注:真是优雅
2016 快速排序
分析:它既要n1-n2的绝对值尽可能小,又要S1-S2的绝对值尽可能大,那不就是最好分成两份差不多均等的,大的放右边,小的放左边嘛,那这是啥?不就是快排嘛
算法思路:
- 初始化变量,
low
和high
用于标记数组的边界,low0
和high0
用于在划分过程中保存边界,k
为要找的第k小的元素的位置,s1
和s2
分别用于存储前k小和后n-k小的元素之和。 - 使用while循环进行快速选择算法的划分过程,直到找到第k小的元素。
- 在每次迭代中,选择
low
位置的元素作为枢轴(pivot),然后移动low
和high
指针来划分数组,使得左边的元素都不大于枢轴,右边的元素都不小于枢轴。 - 如果枢轴元素恰好位于第k小的位置,结束划分;否则,根据枢轴的位置调整
low0
和high0
,然后继续划分。 - 找到第k小的元素后,遍历数组,计算前k小的元素之和与后n-k小的元素之和,返回它们的差值。
int setPartition(int a[], int n) {
int pivotkey, low = 0, low0 = 0, high = n - 1, high0 = n - 1, flag = 1, k = n / 2, i;
int s1 = 0, s2 = 0;
while (flag) {
pivotkey = a[low];
// 选择枢轴
while (low < high && a[high] >= pivotkey) --high;
if (low != high) a[low] = a[high];
while (low < high && a[low] <= pivotkey) ++low;
if (low != high) a[high] = a[low];
// 基于枢轴对数据进行划分
if (low == k - 1) {
// 如果枢轴是第n/2小元素,划分成功,flag=0;
flag = 0;
} else {
// 是否继续划分
if (low < k - 1) {
low0 = ++low;
high = high0;
} else {
high0 = --high;
low = low0;
}
}
}
// 计算前k小的元素之和与后n-k小的元素之和
for (i = 0; i < k; i++) s1 += a[i];
for (i = k; i < n; i++) s2 += a[i];
return s2 - s1;
}
时间复杂度:O(n)
空间复杂度:O(1)
2017 二叉树(InOrder)
分析:要实现将给定的二叉树转换成为等价的中缀表达式并输出,那其实我们只需要中序遍历就好了,一边遍历一边输出,最后的结果自然就是该题想要我们实现的。任务就转换成为对中序遍历稍加改变一下咯。
还有还有为了保证输出出来的中缀表达式有正确的运算顺序,我们还应该在合适的位置添加括号。(遍历左子树之前加上,遍历完右子树之后也加上)
typedef struct node{
char data[];
struct node *left, *right;
}BTree;
//BreeToTree(root,1); //调用该程序
void BreeToTree(BTree *root,int deep){
if(root!=NULL){
if(root->left=NULL&&root->right=NULL){
printf("%s",root->data);
}else{
if(deep>1) printf("("); //遍历左子树之前
BreeToTree(root->left,deep+1); //递归遍历左子树
printf("%s",root->data);
BreeToTree(root->right,deep+1); //递归遍历右子树
if(deep>1) printf(")"); //遍历右子树之后
}
}else{
return; //空结点直接返回
}
}
2018 顺序表
分析:
先想想最容易实现的是什么呢?自然也还是可以设置一个辅助数组专门记录其下标是否出现过,最后在从小到大第一个未被标记过的不就是我们想找的最小正整数吗。
然后我们再看回要求,这题只要求时间上高效喔,那就是极致的空间换时间,快就完事了呗。上述思路的时间复杂度为O(n)嘞,那我们思考下有没有什么是可以更快做出来的呢?是不是也可以直接先过一遍排序(但在这个过程中遇到负数就直接跳过)然后遍历再找一遍是不是也出结果了(不过要是想稳定的话,剩下的几个排序算法的时间复杂度也为O(n))。
注:
书里所给出的和我第一次的想法一样蛤。
int findMissMin(int A[], int n) {
int i;
int *B; // 标记数组
// 分配空间,使用malloc而不是错误的maloc
B = (int *)malloc(sizeof(int) * (n + 1)); // 需要为1到n的整数分配空间
// 赋初值为0,注意memset的第二个参数应该是字节数
memset(B, 0, sizeof(int) * (n + 1));
// 遍历数组A,如果A[i]在1到n之间,则标记B[A[i] - 1]为1
for (i = 0; i < n; i++) {
if (A[i] > 0 && A[i] <= n) {
B[A[i]] = 1; // 直接使用A[i]作为索引,因为我们要标记1到n的整数
}
}
// 扫描数组B,找到第一个值为0的位置,即未出现的最小正整数
for (i = 1; i <= n; i++) { // 从1开始,因为0不是正整数
if (B[i] == 0) {
break;
}
}
// 返回结果,i是数组B中第一个未标记的位置,即未出现的最小正整数
return i;
}
int findMissin(std::vector<int>& nums) {
int n = nums.size();
// 将每个正整数放到对应的索引位置
for (int i = 0; i < n; i++) {
// 只处理在范围内的正整数
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
// 交换到正确的位置
std::swap(nums[i], nums[nums[i] - 1]);
}
}
// 查找第一个缺失的正整数
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1; // 返回缺失的最小正整数
}
}
return n + 1; // 如果所有位置都正确,返回n+1
}
注:第二种的空间复杂度会较低,但是题目也并未要求。
2019 单链表(双指针)
【2019统考真题】设线性表L=(a1,a2,a3,...,an-1,an-1,an)采用带头结点的单链表保存,链
表中的结点定义如下:typedef struct node { int data; struct node* next; }NODE
请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表L=(a1,an,a2,an-1,a3,an-2,...)要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释,
3)说明你所设计的算法的时间复杂度。
分析:这题有硬性要求咯,空间复杂度要为O(1)才行。那其实我们可以借助之前做的10年真题的想法,你看目的是不是要从头和从屁股开始交替形成一个新的链表,那其实我们可以先找到中间(向下取整的那个结点)然后反转后半段,再交替将其插入头结点之后的链表就完事了。
void changeList(NODE* h) {
NODE *p, *q, *r, *s;
p = q = h; // 初始化慢指针和快指针
// 寻找中间节点
while (q->next != NULL) {
p = p->next; // 慢指针走一步
q = q->next; // 快指针走一步
if (q->next != NULL) {
q = q->next; // 快指针再走一步
}
}
// p所指节点为中间节点,q为后半段链表的首节点
NODE* secondHalf = p->next; // 保存后半段链表
p->next = NULL; // 切断前半段链表
// 将链表后半段逆置
NODE* prev = NULL;
while (secondHalf != NULL) {
r = secondHalf->next; // 保存下一个节点
secondHalf->next = prev; // 反转指针
prev = secondHalf; // 移动前驱
secondHalf = r; // 移动到下一个节点
}
// prev 现在指向反转后的后半段链表的头
s = h->next; // s指向前半段的第一个数据节点
q = prev; // q指向后半段的第一个数据节点
// 将链表后半段的节点插入到指定位置
while (q != NULL) {
r = q->next; // r指向后半段的下一个节点
q->next = s->next; // 将q插入到s之后
s->next = q; // 更新s的next指向q
s = q->next; // s指向前半段的下一个插入点
q = r; // 移动到下一个节点
}
}
时间复杂度:O(n)
该算法需要遍历链表三次:一次找到中间节点,一次反转后半部分,一次合并两个链表。
2020 最短路径(Floyd)or 双指针
【2020统考真题】定义三元组(a,b,c)(abc均为整数)的距离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,然后第二层是S2,第三层是S3(由外向内)然后再来个tempmin,暴力枚举出所有的可能性,再随着循环遍历依次比较当前最小,直至结束选出最优解。
// 计算三个数组S1、S2和S3中元素之间的最小距离
int min_dist(int S1[], int S2[], int S3[]) {
// 用于存储产生最小距离的路径
int path[3];
// 初始化路径数组
memset(path, 0, sizeof(path));
// 计算数组S1的长度
int l1 = sizeof(S1) / sizeof(*S1);
// 计算数组S2的长度
int l2 = sizeof(S2) / sizeof(*S2);
// 计算数组S3的长度
int l3 = sizeof(S3) / sizeof(*S3);
// 初始化最小距离为一个很大的数
int md = 0x3FFF; // 使用十六进制表示一个很大的数
// 遍历数组S1
for (int i = 0; i < l1; i++) {
// 遍历数组S2
for (int j = 0; j < l2; j++) {
// 遍历数组S3
for (int k = 0; k < l3; k++) {
// 计算当前路径的总距离
int td = std::abs(S1[i] - S2[j]) + std::abs(S2[j] - S3[k]) + std::abs(S3[k] - S1[i]);
// 如果当前路径的总距离小于已知的最小距离,则更新最小距离和路径
if (td < md) {
md = td;
path[0] = S1[i];
path[1] = S2[j];
path[2] = S3[k];
}
}
}
}
// 输出产生最小距离的路径
for (int i : path) {
std::cout << i << std::endl;
}
// 返回最小距离
return md;
}
更优解:暴力解法在集合大小较大时效率较低。一个可能的优化方法是使用双指针技巧,首先固定一个集合中的元素,然后在另外两个集合中使用双指针来寻找最小距离。这种方法可以减少一些不必要的计算,但仍然需要O(n1 * n2 + n2 * n3 + n3 * n1)的时间复杂度。如果S1、S2和S3中的元素都是有序的,那么双指针方法可以更有效地找到最小距离。(书里那个例图其实画的很清楚蛤)
int findMinDistance(const std::vector<int>& S1, const std::vector<int>& S2, const std::vector<int>& S3) {
int minDist = std::numeric_limits<int>::max(); // 初始化最小距离为最大值
int n1 = S1.size(), n2 = S2.size(), n3 = S3.size();
// 遍历S1中的每个元素
for (int a : S1) {
int i = 0, j = 0; // 初始化指针
// 双指针遍历S2和S3
while (i < n2 && j < n3) {
int b = S2[i];
int c = S3[j];
// 计算当前三元组的距离
int dist = std::abs(a - b) + std::abs(b - c) + std::abs(c - a);
minDist = std::min(minDist, dist); // 更新最小距离
// 移动指针,寻找更优解
if (b < c) {
i++; // b较小,向右移动S2的指针
} else {
j++; // c较小或相等,向右移动S3的指针
}
}
}
return minDist;
}
注:该算法的思维还是很重要的,很多场合都可以用得到。
2021 图(邻接矩阵)
1)算法的基本设计思想
本算法题属于送分题,题干已经告诉我们算法的思想。对于采用邻接矩阵存储的无向图,在邻接矩阵的每一行(列)中,非零元素的个数为本行(列)对应顶点的度。可以依次计算连通图G中各顶点的度,并记录度为奇数的顶点个数,若个数为0或2,则返回1,否则返回0。
2)算法实现
int IsExistEL(MGraph G)(//采用邻接矩阵存储,判断图是否存在EL路径int degree, i, j, count=0;
for(i=0;i<G. numVertices;i++){
degree=0;
for(int j=0;j<G.numVertices;j++){
degree+=G.Edge[i][j]; //依次计算各个顶点的度
if(degree%2!=0) count++; //对度为奇数的顶点计数1
}
if(count==0||count==2)
return 1; //存在EL路径,返回1
else
return 0; //不存在EL路径,返回0
}
}
3)时间复杂度和空间复杂度
算法需要遍历整个邻接矩阵,所以时间复杂度是O(n^2),空间复杂度是O(1)。
2022 二叉树(BST)
【2022统考真题】已知非空二叉树T的结点值均为正整数,采用顺序存储方式保存,数据结构定义如下:
分析:都说是二叉排序树了嘛,那我就只需要在遍历过程中看一下是否有不满足定义(左边大于中间或者右边小于中间)的结点就好啦。那其实是不是也可以直接中序遍历完看序列是不是递增的就好啦。
// 检查数组是否表示一个二叉搜索树
int IsBST(int* SqBiTNode, int size) {
int* pmax = (int*)malloc(size * sizeof(int));
int* pmin = (int*)malloc(size * sizeof(int));
if (!pmax || !pmin) {
free(pmax);
free(pmin);
return 0;
}
// 初始化pmax和pmin数组
for (int i = 0; i < size; i++) {
int iLeft = 2 * i + 1;
int iRight = 2 * i + 2;
if (iLeft < size) pmax[i] = SqBiTNode[iLeft];
if (iRight < size) pmin[i] = SqBiTNode[iRight];
if (iLeft < size && SqBiTNode[i] <= pmax[iLeft]) return 0;
if (iRight < size && SqBiTNode[i] >= pmin[iRight]) return 0;
}
free(pmax);
free(pmin);
return 1; // 所有节点都满足BST的条件
}
// 检查数组是否表示一个二叉搜索树
int IsBST(int* SqBiTNode, int size) {
int val = INT_MIN; // 初始化为最小整数
for (int i = 0; i < size; i++) {
if (SqBiTNode[i] <= val) return 0; // 如果当前节点值小于等于val,则不是BST
val = SqBiTNode[i]; // 更新val为当前节点值
}
return 1; // 中序遍历得到的序列是升序的,因此是BST
}
2023 图(邻接矩阵)
(1)算法思想:
- 遍历有向图中所有顶点,并统计各顶点的出度和入度,输出出度大于入度的KJ页点,并使用变量 count 累计顶点的总数。
- 计算顶点i的出度: 遍历邻接矩阵的i行元素,即 Edge[i][0]~Edge[i][numVertex-1],统计非零元素个数,即为顶点i的出度
- 计算顶点i的入度:遍历邻接矩阵的i列元素,即Edge[0][i]~ Edge[numVertex-1][i],统计非零元素个数,即为顶点i的入度
(2)算法实现:
int printVertices (MGraph G){
int count =0;//K顶点的总数
for (int i=0; i<G.numVertex; i++){
int outDegree = 0;//顶点i的出度
int inDegree = 0;//顶点i的入度
for (int j=0;j<G.numVertex; j++)
if (G.Edge[i][j]>0) outDegree++;
}
for (int j=0;j<G.numVertex; j++)
if (G.Edge[j][i]>0) outDegree++;//循环两次方便理解
}
if (outDegree > inDegree) [//顶点i的出度大于入度
printf ("c\n",G.VertexList[i]);//输出顶点i
count++;//累加K顶点总数
}
}
return count;//返回x顶点总数
}
2024 图(邻接矩阵)and 拓扑排序
41.已知有向图G采用邻接矩阵存储,类型定义如下
typedef struct{ //图的类型定义 int numVertices,numEdges; //图的顶点数和有向边数 char VerticesList[MAXV]; //顶点表,MAXV为已定义常量 int Edge[MAXV][MAXV]; //邻接矩阵 }MGraph;
请设计算法:int uniquely(MGraph G),判定G是否存在唯一的拓扑序列,若是,则返回1,否则返回0,要求如下:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
分析:因为给的是邻接矩阵,那就是要求咱考虑一下怎么着才能根据邻接矩阵的特性找到唯一的拓扑排序咯。首先我们想下拓扑排序是咋出来的呢,是不是找那个没有人管的结点(入度为0的),然后将其输出,重复这个过程。如果能够全部输出就表明存在拓扑排序。那很自然就顺到要找入度为0的点了对吧,邻接矩阵找入度为0还不简单吗,就拿一个数组专门存起来呗。然后再考虑一下这其中的细节问题就是当我输出一个点的时候,相邻结点的入度得减1,这个就是根据邻接矩阵的特性我看看aij存不存在咯,存在就证明有边嘛。那如何保证其唯一性呢?就是在同一时刻不允许出现两个入度为0的结点。
基本思路:
- 创建一张入度表indegree[]用于记录各个顶点的入度情况
- 选择入度为0的点并将所有邻接结点的入度-1(借助邻接矩阵实现)
- 重复以上过程,若每次选中的入度为0的顶点有且仅有一个且全部遍历完,则存在唯一的拓扑排序;反之不存在或存在多个拓扑排序。
// 声明入度数组并分配内存
int *indegree;
indegree = (int *)malloc(G.numVertices * sizeof(int)); // 为入度数组分配内存
// 初始化入度数组
for (int j = 0; j < G.numVertices; j++) {
indegree[j] = 0; // 将每个顶点的入度初始化为0
// 计算每个顶点的入度
for (int i = 0; i < G.numVertices; i++) {
indegree[j] += G.Edge[i][j]; // 累加所有指向顶点j的边的数量
}
}
// 拓扑排序相关变量
int count = 0; // 已处理顶点的计数
int zero_count, zero_vertex; // 用于存储入度为0的顶点数量和其索引
while (count < G.numVertices) { // 当已处理的顶点数量小于总顶点数量时
zero_count = 0; // 重置入度为0的顶点计数
zero_vertex = -1; // 初始化入度为0的顶点索引
// 寻找入度为0的顶点
for (int j = 0; j < G.numVertices; j++) {
if (indegree[j] == 0) { // 如果顶点j的入度为0
zero_count++; // 增加入度为0的顶点计数
zero_vertex = j; // 记录该顶点的索引
if (zero_count > 1) break; // 如果找到多个入度为0的顶点,退出循环
}
}
// 如果有多个或没有入度为0的节点
if (zero_count != 1) { // 如果入度为0的顶点数量不等于1
free(indegree); // 释放内存
return 0; // 返回错误码,表示拓扑排序失败
}
// 移除找到的入度为0的顶点,并更新相邻节点的入度
count++; // 增加已处理顶点的计数
indegree[zero_vertex] = -1; // 标记该顶点为已处理
for (int j = 0; j < G.numVertices; j++) {
if (G.Edge[zero_vertex][j] > 0) { // 如果存在从zero_vertex到j的边
indegree[j]--; // 更新相邻顶点j的入度
}
}
}