《数据结构》(408代码题)

2009 单链表(双指针)

分析:首先呢,给我们的数据结构是一个带有表头结点的单链表,也不允许我们改变链表的结构。链表的长度不是直接给出的啊,所以这个倒数也很棘手。那我们该如何解决这个“k”呢,我们并不能一下子就知道这个倒数第k位置在哪里,不过不妨倒着想一下,如果现在有一个指针指向尾结点,又有一个指针指向倒数第k个。那我们再逆推一下过程这两个指针一起往回走,当先前指向倒数第k个结点的指针走到表头与list相会时,后面那个指针是不是也到正数第k个结点的头上了?那是不是我们的问题就解决了。

思路:设置两个指针p、q,初始时都指向list,让q先往后走k步(计数器实现),这时再让p、q同时朝后走,直至q到达尾指针(所指的next为空),那么此时此刻的p所指向的结点既是我们所需要的倒数第k个结点,将其data值输出。

详细实现步骤

  1. 初始化指针: 设置两个指针pq,初始时都指向链表的头节点list
  2. 移动快指针: 让指针q向前移动k步。这里需要注意的是,如果k大于链表的长度,那么查找失败,因为不存在倒数第k个节点。
  3. 同步移动双指针: 当q成功移动了k步并且q不为空(即没有到达链表末尾)时,开始同步移动pq两个指针。每次循环,pq都同时向后移动一步,即p = p->next;q = q->next;
  4. 查找结束条件: 当指针q到达链表的末尾(即q->nextNULL)时,停止移动。此时,p所指向的节点就是链表中倒数第k个节点。
  5. 检查并输出结果: 检查指针p是否为NULL。如果p不为空,说明找到了倒数第k个节点,输出该节点的data域的值,并返回1表示成功。如果p为空,说明没有找到倒数第k个节点,返回0表示失败。
  6. 返回结果: 函数返回查找的结果,即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、算法思想

  1. 反转数组:首先反转整个数组 R。
  2. 反转前 n−p 个元素:接着反转数组的前 n−p 个元素。
  3. 反转后 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) 
    • 常数空间使用:该算法仅使用了几个额外的变量(如 tempstartend),这些变量的数量与输入数组的大小无关。
    • 不使用额外数组:算法在进行反转时,直接在原数组上进行操作,而不需要创建新的数组来存放中间结果。

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

算法的基本设计思想

  1. 确定较长链表: 首先确定两个链表中较长的一个,因为共同后缀不可能长于较短的链表。
  2. 移动较短链表的指针: 将较短链表的指针移动到与较长链表相同长度的位置。
  3. 同时遍历: 从这个点开始,同时遍历两个链表,比较每个节点的值。
  4. 找到共同后缀: 当两个指针同时到达链表末尾或者发现不匹配的节点时,停止遍历。如果到达末尾,说明找到了共同后缀;否则,记录下不匹配时两个链表的指针位置。

算法的详细实现步骤

  1. 计算两个链表的长度。
  2. 使用较长链表的长度减去较短链表的长度,得到需要前进的步数。
  3. 将较长链表的头指针移动到倒数第k个节点,其中k为需要前进的步数。
  4. 同时遍历两个链表,直到两个指针都到达末尾或者发现不匹配的节点。
  5. 如果两个指针都到达末尾,返回较长链表的当前节点,即为共同后缀的起始位置。
  6. 如果发现不匹配的节点,返回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 的统计数组并初始化为 0int *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. 候选元素的选取

    • 使用一个计数器来记录当前候选元素的“支持度”。
    • 遍历数组时,如果计数器为零,选择当前元素作为新的候选元素,并将计数器设为1。
    • 如果当前元素与候选元素相同,增加计数器;如果不同,减少计数器。
  2. 为什么有效

    • 如果存在一个元素的出现次数超过 n/2,那么在遍历过程中,该元素将始终保持为候选元素。
    • 计数器的增加和减少保证了在元素数量相等时,候选元素的优势可以保持。

具体步骤:

  1. 初始化

    • 设置一个候选元素 candidate 和一个计数器 count,初始值为 0。
  2. 遍历数组

    • 对于每个元素:
      • 如果 count 为 0,更新 candidate 为当前元素,并将 count 设置为 1。
      • 如果当前元素等于 candidate,则 count 加 1。
      • 如果当前元素不等于 candidate,则 count 减 1。
  3. 验证候选元素

    • 遍历数组,统计 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和当前深度depthif (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的绝对值尽可能大,那不就是最好分成两份差不多均等的,大的放右边,小的放左边嘛,那这是啥?不就是快排嘛

算法思路

  1. 初始化变量,low 和 high 用于标记数组的边界,low0 和 high0 用于在划分过程中保存边界,k 为要找的第k小的元素的位置,s1 和 s2 分别用于存储前k小和后n-k小的元素之和。
  2. 使用while循环进行快速选择算法的划分过程,直到找到第k小的元素。
  3. 在每次迭代中,选择low位置的元素作为枢轴(pivot),然后移动lowhigh指针来划分数组,使得左边的元素都不大于枢轴,右边的元素都不小于枢轴。
  4. 如果枢轴元素恰好位于第k小的位置,结束划分;否则,根据枢轴的位置调整low0high0,然后继续划分。
  5. 找到第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而不是错误的malocB = (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]为1for (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指向qs = 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; // 使用十六进制表示一个很大的数// 遍历数组S1for (int i = 0; i < l1; i++) {// 遍历数组S2for (int j = 0; j < l2; j++) {// 遍历数组S3for (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和S3while (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路径,返回1else    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,则不是BSTval = 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]);//输出顶点icount++;//累加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的入度为0zero_count++; // 增加入度为0的顶点计数zero_vertex = j; // 记录该顶点的索引if (zero_count > 1) break; // 如果找到多个入度为0的顶点,退出循环}}// 如果有多个或没有入度为0的节点if (zero_count != 1) { // 如果入度为0的顶点数量不等于1free(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的入度}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/490595.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在 Visual Studio Code 中编译、调试和执行 Makefile 工程 llama2.c

在 Visual Studio Code 中编译、调试和执行 Makefile 工程 llama2.c 1. Installing the extension (在 Visual Studio Code 中安装插件)1.1. Extensions for Visual Studio Code1.2. C/C1.2.1. Pre-requisites 1.3. Makefile Tools 2. Configuring your project (配置项目)2.1.…

ur机器人ros-urdf

新建工作空间 mkdir -p ~/catkin_ws/src cd catkin_ws_ur5/src git clone -b melodic-devel https://github.com/ros-industrial/universal_robot.git cd .. rosdep update rosdep install --rosdistro melodic --ignore-src --from-paths src catkin_make source ~/catkin_ws…

Leonardo.Ai丨一键生成图片(AI绘图)

随着人工智能技术的迅速发展,AI在各个领域的应用越来越广泛,特别是在图像生成方面。AI艺术创作的崛起,不仅让艺术创作变得更加便捷和创新,也为设计师、艺术家及普通用户提供了全新的工具。Leonardo.Ai作为一款基于人工智能的图像生成工具,通过简洁的操作和强大的功能,成功…

【前端开发】HTML+CSS网页,可以拿来当作业(免费开源)

HTML代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content_lizhongyu"widthdevice-width, initial-scale1.0"><title>小兔鲜儿-新鲜、惠民、快捷<…

AI学习记录 - 依据 minimind 项目入门

想学习AI&#xff0c;还是需要从头到尾跑一边流程&#xff0c;最近看到这个项目 minimind, 我也记录下学习到的东西&#xff0c;需要结合项目的readme看。 1、github链接 https://github.com/jingyaogong/minimind?tabreadme-ov-file 2、硬件环境&#xff1a;英伟达4070ti …

【STM32】RTT-Studio中HAL库开发教程九:FLASH中的OPT

文章目录 一、概要二、内部FLASH排布三、内部FLASH主要特色四、OTP函数介绍五、测试验证 一、概要 STM32系列是一款强大而灵活的微控制器&#xff0c;它的片内Flash存储器可以用来存储有关代码和数据&#xff0c;在实际应用中&#xff0c;我们也需要对这个存储器进行读写操作。…

长安大学《2024年812自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《长安大学812自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2024年真题 Part1&#xff1a;2024年完整版真题 2024年真题

说说你对java lambda表达式的理解?

大家好&#xff0c;我是锋哥。今天分享关于【说说你对java lambda表达式的理解?】面试题。希望对大家有帮助&#xff1b; 说说你对java lambda表达式的理解? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Java Lambda 表达式是 Java 8 引入的一项重要特性&#…

【PostgreSQL使用】最新功能逻辑复制槽的failover,大数据下高可用再添利器

逻辑复制的failover ​专栏内容&#xff1a; postgresql入门到进阶手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. ✅ &#x1f52…

【leetcode100】环形链表Ⅱ

1、题目描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统…

决策曲线分析(DCA)中平均净收益用于评价模型算法(R自定义函数)

决策曲线分析&#xff08;DCA&#xff09;中平均净收益用于评价模型算法 DCA分析虽然不强调用来评价模型算法或者变量组合的优劣&#xff0c;但是实际应用过程中感觉DCA曲线的走势和模型的效能具有良好的一致性&#xff0c;其实这种一致性也可以找到内在的联系&#xff0c;比如…

【人工智能】OpenAI O1模型:超越GPT-4的长上下文RAG性能详解与优化指南

在人工智能&#xff08;AI&#xff09;领域&#xff0c;长上下文生成与检索&#xff08;RAG&#xff09; 已成为提升自然语言处理&#xff08;NLP&#xff09;模型性能的关键技术之一。随着数据规模与应用场景的不断扩展&#xff0c;如何高效地处理海量上下文信息&#xff0c;成…

基于智能电能表的智能家居能源管理系统设计

目录 引言系统设计 硬件设计软件设计系统功能模块 电能测量模块数据传输模块能源管理模块控制算法 数据采集与处理算法能源优化算法代码实现 电能测量模块实现数据传输模块实现系统调试与优化结论与展望 1. 引言 随着智能家居的发展&#xff0c;电能管理成为智能家居系统中的…

GLM-4-Plus初体验

引言&#xff1a;为什么高效的内容创作如此重要&#xff1f; 在当前竞争激烈的市场环境中&#xff0c;内容创作已成为品牌成功的重要支柱。无论是撰写营销文案、博客文章、社交媒体帖子&#xff0c;还是制作广告&#xff0c;优质的内容不仅能够帮助品牌吸引目标受众的注意力&a…

软考高级架构 - 10.5 软件架构演化评估方法

10.4 软件架构演化原则总结 本节提出了18条架构演化的核心原则&#xff0c;并为每条原则设计了简单而有效的度量方法&#xff0c;用于从系统整体层面提供实用信息&#xff0c;帮助评估和指导架构演化。 演化成本控制&#xff1a;成本小于重新开发成本&#xff0c;经济高效。进…

科研笔记:ARR 与 ACL rolling

1 ARR 介绍 ARR 提供 评审服务 —— 仅限评审 —— 对于提交的论文。评审不会针对特定会议/场所&#xff0c;但评审标准与传统会议的主会场长文或短文提交要求相同&#xff08;如 ACL 或其他由 ACL 主办的重要会议&#xff09; 2 提交论文进行 ARR 评审 提交截止日期 每两个…

9_less教程 --[CSS预处理]

LESS&#xff08;Leaner Style Sheets&#xff09;是一种CSS预处理器&#xff0c;它扩展了CSS语言&#xff0c;增加了变量、嵌套规则、混合&#xff08;mixins&#xff09;、函数等功能&#xff0c;使得样式表的编写更加灵活和易于维护。下面是一些LESS的基础教程内容&#xff…

JVM 双亲委派模型以及垃圾回收机制

目录 1. JVM 内存区域划分 2. JVM 中类加载的过程 1) 类加载的基本流程 2) 双亲委派模型 3. JVM 中垃圾回收机制 1) 找到垃圾 a) 引用计数 b) 可达性分析 2) 释放垃圾 1. JVM 内存区域划分 一个运行起来的 Java 进程&#xff0c;其实就是一个 JVM 虚拟机。 而进程是…

leetcode-73.矩阵置零-day5

class Solution {public void setZeroes(int[][] mat) {int m mat.length, n mat[0].length;// 1. 扫描「首行」和「首列」记录「首行」和「首列」是否该被置零boolean r0 false, c0 false;for (int i 0; i < m; i) {if (mat[i][0] 0) {r0 true;break;}}for (int j …

C++11语法解析(二)

可变参数模板 基本语法及原理 ・C11 支持可变参数模板&#xff0c;也就是说支持可变数量参数的函数模板和类模板&#xff0c;可变数目的参数被称为参数包&#xff0c;存在两种参数包&#xff1a;模板参数包&#xff0c;表示零或多个模板参数&#xff1b;函数参数包&#xff1…