四川大学874数据结构算法题
2015篇
1.有一个含有 n 个整数的无序数据序列,所有数据元素各不相同,采用整数数组 R[0, n-1] 存储。要求:
(1). 设计一个尽可能高效的算法,输出序列中第 k(1≤k≤n) 小的元素
- 两种思路:
- 建立一个含 k 个元素的小顶堆进行堆排。堆中保留最大的 k 个数,堆顶即为第 k 小元素;
- 采用类似快排的轴枢划分策略,每次划分可以确定一个元素位置。当轴枢为第 k 个位置时轴枢值即为第 k 小的元素;
(2). 给出求解过程并分析时间复杂度。
- 对于方法1:时间复杂度为 \(n*log_2k\)
- 代码:
void PrintNumK(int k, int n) { // 输出第 k 小的数 // 建立一个长度为 k 的数组堆排序,每次用原数组的下一个元素换掉堆顶最大元素,数组遍历完后堆顶元素就是目标元素; int Heap[k+1]; int i; for (i = 0; i < k; i++) Heap[i+1] = nums[i]; for (i = k/2; i > 0; i--) BHeapDownAdjust(Heap, i, k); for (i = k; i < n; i++) { if(Heap[1] > nums[i]) Heap[1] = nums[i]; //外部元素比堆顶小时,把堆顶元素换掉 else continue; BHeapDownAdjust(Heap, 1, k); } printf("%d",Heap[1]); } void BHeapDownAdjust(int Heap[], int l, int h) { //j将Heap[l]为根节点的子树调整为大顶堆 //Heap[l] 元素的下沉过程 // l 和 h 为数组调整范围的 低位 和 高位 int i=l, j=2*l; int temp = Heap[i]; while (j<=h) { if (j<h && Heap[j]<Heap[j+1]) j++; if (temp<Heap[j]) { Heap[i] = Heap[j]; i = j; //继续检查下沉的节点是否还需要下沉 j = 2 * i; } else break; } Heap[i] = temp; }
- 对于方法2:平均复杂度为O(n)
- 代码:
int SNum_k(int *nums, int low, int high, int k) { // 输出第nums中第 k 小的数 if (low>high) return -1; // > 而不是>=; =时还需要走一次 int L = low, H = high; int temp = nums[L]; while(L<H) { while(L<H && nums[H]>temp) --H; nums[L] = nums[H]; while(L<H && nums[L]<temp) ++L; nums[H] = nums[L]; } nums[L] = temp; if(L==k-1) printf("%d\n ",temp); //第 k 小的数应在 k-1 的位置 if(L<k-1) SNum_k(nums, L+1, high, k); else SNum_k(nums, low, L-1, k); }
2.以带有双亲指针的二叉树作为二叉树的存储结构,节点类型如下:
typedef struct node
{
int data;
struct node *lchild, *rchild;
struct node *parent;
}BinTNode;
1.就中序序列的不同情况,简要叙述求中序后继的方法
- 两种情况:
- 没有右孩子,则 后继节点 是第一个以当前节点所在子树为 左子树 的节点;
- 有右孩子,后继节点为右子树中最左的节点。
2.编写算法求px所指节点的中序后继节点
- 代码
BinNode *InorderNext(BinNode *px) { if(px == NULL) retuen NULL; BinNode *p = px; if(px->rchild != NULL) //有右孩子时 { p = px->rchild; while(p->lchild != NULL) p = p->lchild; return p; } else // 无右孩子时 { while(p->parent->lchild!=p && p->parent!=NULL) p = p->parent; return p->parent; } }
3.只使用孩子指针而使用双亲指针,编写算法输出从 root 到 px 的路径;
- 思路:中序遍历,用栈保存经过的路径
- 代码:
void PrintPath(BinNode *root, BinNode *px) { static int path[maxsize]; // maxsize 为已定义的满足长度的常数 static int top=0; static int flag=1; // 路径找到标志, if(root && flag) return; // flag为0时,不需要再向下遍历,直接返回 path[top++] = root->data; if(root == px) // 满足条件 { for(int i = 0; i < top; i++) print("%d ",path[i]); flag = 0; //flag 置 0 ,截断遍历 } PrintPath(root->lchild); PrintPath(root->rchild); top--; }
2016篇
1. 一个带头节点的单链表(Linklist),结构为[data;next],在不改变链表的情况下,查找链表中倒数第 k (正整数) 个位置上的节点; 查找成功,输出节点的data值,返回1; 否则,只返回0;
-
1.思想:设置两个指针,第一个指针先走 k 步(若指针指向空了说明 k 非法),指向不包括头节点在内的第k个节点;此时第二个指针指向头节点的下一个(不包含头节点的第一个节点),然后两个指针同步遍历,当第一个指针的 下一个节点 为空时,第二个指针指向的即为倒数第 k 个元素;
-
2.代码:
int SearchReNum_K(Linklist *head, int k) { //打印链表倒数第 k 个元素 Linklist *p1=head, *p2=head->next; for(int i=0; i<k ;i++) // 将 p1 定位到第 K 个节点 { p1 = p1->next; if(p1 == NULL) return 0; // k 值不合法 } while(p1->next != NULL) // 遍历到 p1 指向尾节点为止 { p1 = p1->next; p2 = p2->next; } print("%d ", p2->data); return 1; }
2. 求带权有向图的最小路径,要求时间复杂度为O((V+E)logK).
- 思路: 建立一个堆对边按权值进行堆排序,堆内节点为{node;cost},根据cost排序。问题在于排序完成后边在堆中的位置发生了变化,更新权值时如何定位到对应的边的位置,遍历堆查找应该是最坏情况(结果还是遍历了)
- 时间复杂度:应该还是做不到题目要求,总的遍历次数 E 次,单每次遍历的最大开销应该是堆中在遍历找边的时候 k;
- 代码:
typedef struct { //堆节点 int cost; //权值 int vex; //顶点 }HeapNode; void SHeapUpAdjust(HeapNode* Heap, int k) { //小顶堆单个元素的上浮过程,k上浮元素位置 int i=k, j=k/2; HeapNode temp = Heap[k]; while (j>0) { if (temp.cost < Heap[j].cost) { Heap[i] = Heap[j]; i = j; //继续检查节点是否还需要上浮 j = j/2; } else break; } Heap[i] = temp; } void SHeapDownAdjust(HeapNode* Heap, int k, int h) { // 小顶堆单个元素的下沉过程,k: 要调整的元素位置; h: 堆的总长度 int i = k, j = 2*k; HeapNode temp = Heap[i]; while(j<=h) { if (j<h && Heap[j].cost > Heap[j+1].cost) j++; if (temp.cost > Heap[j].cost) { Heap[i] = Heap[j]; i = j; j = 2*j; } else break; } Heap[i] = temp; } void HeapDijkstra(AGraph *G, int v) { int dist[maxsize], path[maxsize], isJoin[maxsize]; int num = 0; //堆中有效节点个数 int pos; //定位节点在堆中的位置 ArcNode *p; HeapNode heap[maxsize+1]; for (int i = 0; i < maxsize+1; i++) //初始化数组 { if (i<maxsize) { dist[i] = MAX; path[i] = -1; isJoin[i] = 0; } heap[i].cost=MAX; // 初始cost为MAX heap[i].vex = -1; // 节点初始化为-1 } dist[v] = 0; path[v] = -1; isJoin[v] = 1; for (int i = 0; i < G->v - 1; ++i) { p = G->Adjlist[v].firstarc; while (p!=NULL) // 节点入堆 { if(isJoin[p->vex]) //若边已加入,跳过 { p=p->nextarc; continue; } pos = 0; for (int k = 1; k < num+1; k++) //定位节点在heap中的位置 { if(heap[k].vex==p->vex) { pos = k; break; } } if(pos && p->cost + dist[v] < heap[pos].cost)//堆中已有p->vex,且v到p->vex + dist[v] 的距离小于现存距离,更新权值 { heap[pos].cost = p->cost+dist[v]; path[p->vex] = v; //记录新路径 } if(!pos) // 堆中无p->vex 点, 有效节点数num+1,在新位置填入新节点 { heap[++num].vex = p->vex; heap[num].cost = p->cost + dist[v]; path[p->vex] = v; } SHeapUpAdjust(heap, num); // 堆底上浮 p=p->nextarc; } dist[heap[1].vex] = heap[1].cost; v = heap[1].vex; // 以堆顶节点开始下一次 isJoin[v] = 1; heap[1] = heap[num--]; //换掉堆顶节点,有效节点数-1 SHeapDownAdjust(heap, 1, num); //堆顶下沉 } }
2017篇
1.关于堆,问:
- 1.存储方式是顺序的还是链接。 顺序
- 2.最大元素和最小元素在说明位置。 最大:堆顶; 最小:叶子节点
- 3.对 n 个元素的建队过程中,最多和最少进行多少次比较;
- 最少:堆原始有序,从 n/2 开始比较; 若 n 为奇数,共 \(\frac{n}{2}*2 = n-1\) 次;若 n 为偶数,最后一个节点只比较一次,共 \(\frac{n}{2}*2-1 = n-1\) 次。
- 最多:假设堆是完全二叉树;树高 \(h = log_2n+1\), 从 h-1 层开始比较
- 讨论最多情况,第\(h-1\) 层 每个节点 只需与下一层比较,每个比较2次,共 \(2*2^{h-2}*1\)
- 第\(h-2\) 层除了和下一层比较之外,下坠后 还需和下层继续比较, 共 \(2*2^{h-3}*2\)
- 则第 k 层的元素,最多需要比较的次数为 \(2*2^{k-1}*(h-k)\)
- 总次数为\(\sum_{k=1}^{h-1}(2*2^{k-1}*(h-k))\) = \(2^{h+1}-2(h+1)\)
2. 两个非空带头节点的增序单链表 ha,hb,求两个链表的交集并将结果存放与 hc 中;要求不能破坏原来的链表。节点结构为:
typedef struct Node
{
int data;
struct Node *next;
}Node, *LinkList;
- 设计一个高效算法并给出时间复杂度
Node *Intersection(LinkList ha, LinkList hb) { // 返回两个链表的交集, 带头节点的链表 LinkList hc = (Node *)malloc(sizeof(Node)); Node *pa = ha->next, *pb = hb->next, *p, *pre; hc->next = NULL; while(pa && pb) { if(pa->data == pb->data) { p = (Node *)malloc(sizeof(Node))); p->data = pa->data; p->next = NULL; if(hc->next == NULL) //第一个节点特殊处理 { hc->next = p; pre = p; } else // 后续节点尾插法 { pre->next = p; pre = p; } } else if(pa->data > pb->data) pb = pb->next; // 较小的节点向后走 else pa = pa->next; } return hc; }
3 设计一个算法判断 无向图 中是否有环
- 思路1: 统计图中 边的个数n 和 节点的个数m,若n>m-1 则说明存在环路
- 代码:
int DetectLoop(AGraph *G, int v) { static int visit[maxsize]; static int edge, vertex; ArcNode *p = G->Adjlist[v].firstarc; visit[v] = 1; vertex++; // 记顶点 while(p!=NULL) { edge++; //记边 if(visit[p->vex]==0) DetectLoop(G, p->vex); p = p->nextarc; } if(edge/2 >= vertex) { printf("YES"); return 1; } return 0; }
- 思路2:深度遍历,需要将上一个过来的节点标记
- 代码:
int DetectLoop2(AGraph *G, int v, int pre) { //pre:上一个节点 static int visit[maxsize]; static int flag=0; // 环标记 if(flag) return flag; ArcNode *p = G->Adjlist[v].firstarc; visit[v] = 1; while(p!=NULL) { if(p->vex!=pre && visit[p->vex]) flag = 1; //找到环路 if(!visit[p->vex]) DetectLoop2(G, p->vex, v); p = p->nextarc; } return flag; }
扩展.设计一个算法判断 有向图 中是否有环
- 思路:回溯深度遍历,若一个节点在深度遍历完成之前再次回到了自己,说明存在环路;
与普通深度遍历的区别:常规深度遍历会在访问一个节点后将其标记为已访问,下次遇到时不会访问它;
而回溯深度遍历增加一个标志 标记本轮遍历的节点。若某个节点在一轮遍历中被访问两次,说明有环; - 代码:
- 图结构
typedef struct ArcNode { //边节点 int vex; //当前边指向的节点信息 int cost; //边代价 struct ArcNode *nextarc; //下一条边 }ArcNode; typedef struct VNode { //顶点节点 int vex; //顶点数据 ArcNode *firstarc; // 顶点出来的第一条边 }VNode; typedef struct AGraph { //邻接表 VNode Adjlist[maxsize]; //顶点数组 int v,e; //当前顶点的顶点数和边数 }AGraph;
- 代码
int DetectCircle(AGraph *G, int v) { // 基于DFS 检测有向图中是否存在环路; static int visit[maxsize]; static int finished[maxsize]; static int flag = 0; //标记位 if (flag) return 1; //flag 用于对遍历进行截支,满足条件直接返回,不再继续遍历 ArcNode *p = G->Adjlist[v].firstarc; visit[v] = 1; finished[v] = 1; // finished[i]=1 表示还在本轮遍历之中,再次访问到自己说明存在环路 //printf("%d ",v); while(p!=NULL) { if (finished[p->vex]==1) { flag=1; // 同一个节点被再次访问, 说明存在环路; return 1; // 已检测到回路直接返回; } if(visit[p->vex]==0) { DetectCircle(G,p->vex); finished[p->vex]=0; } p = p->nextarc; } return flag; }
2018篇
1.有n个顶点的有向图用邻接表表示,写一个算法求顶点 k 的入度
- 遍历邻接表计数求和, 记邻接表为
Adjlist
- 代码
int InDegree(AGraph *G, int k) { ArcNode *p; int sum=0; for(int i=0; i< G->n; i++) { p = G->Adjlist[i].firstarc; while(p != NULL) { if(p->data == k) { sum++; break; } p = p->next; } } return sum; }
2.递归求一颗树中各个节点的平衡因子
- 深度遍历:一个节点的平衡因子等于 左子树高度 减去 右子树高度;
- 节点结构
{ lchild; rchild; bf; data}
- 代码:
int BalanceNum(BinNode *root) { if(root==NULL) return 0; int LD, RD; LD = BalanceNum(root->lchlid); RD = BalanceNum(root->rchlid); root->bf = LD - RD; return (LD>RD)?(LD+1):(RD+1); //返回较大的 + 1 作为上一层的树高 }
2019篇
1.深度优先遍历实现有向图的拓扑排序
- 思路:在遍历退出的时候记录节点,可以得到一个逆拓扑排序序列,逆序输出即可得到一个拓扑排序
- 代码:
TopoSort(Grapg *G, int v) { static int topo[maxsize]; static int top = 0; static int visit[maxsize]; ArcNode *p = G->Adjlist[v].firstarc; visit[v] = 1; while( p!= NULL) { if(!visit[p->vex]) TopoSort(G, p->vex); p = p->nextarc; } topo[top++] = v; //递归退出节点时节点入栈,栈序列为逆拓扑序列 if(top == maxsize) //逆序输出 for(int i=top-1; i>=0; i--) print("%d ", topo[i]); }
2.用单链表表示任意长度的整数,每个节点存储一位整数。实现两个正整数的减法操作;
- 代码
Node *Minus(Node *LA, Node *LB) { //LA - LB LA = Reverse(LA); //反转数组 ,从地位到高位依次相减 LB = Reverse(LB); int carry=0; //carry为借位,不够减的时候需要向下一位借位 int i; Node *pla = LA; Node *la = LA->next; Node *lb = LB->next; while (la && lb) { if (la->d < lb->d) //如果不够减产生借位 { la->d = la->d - lb->d + 10 - carry; carry = 1; } else { la->d = la->d - lb->d - carry; carry = 0; } la = la->next; lb = lb->next; pla = pla->next; } if (la==NULL) //a 的位数比 b 少的时候, { pla->next = lb; while (lb!=NULL) { lb->d = 0 - lb->d + 10 - carry; carry = 1; lb = lb->next; } } else //a 的位数比 b 多的时候, { while (la!=NULL && carry) { if(la->d==0) { la->d = la->d + 10 - carry; } else { la->d = la->d - carry; carry=0; } la = la->next; } } la = LA->next; if (carry==1) //最后 carry 为 1 的话说明最高位仍产生借位 ,结果为负数 { /* 比如 123 - 456 = -333,反转后对应位相减结果为 321 - 654 = 766; carry=1,表示最终结果是负数;处理方法是对所有位 9-x;最后第一位再 +1; 9-7+1=3; 9-6=3; 9-6=3 结果为 -333; */ while (la!=NULL) { la->d = 9 - la->d; //用 9 减所有位 la = la->next; } LA->next->d++; //个位数修正 } la = LA; la = Reverse(la); while (la->next->d == 0) // 将头节点定位到第一个非 0 元素前面,结果为0要特殊考虑 { if(la->next->next!=NULL) la = la->next; else { la->next->d = 0; break; } } if (carry==1) // 用头节点标记正负 la->d = -1; else la->d = 1; Show(la); //返回结果链表头节点,头节点为-1表示结果为负数,1表示结果非负; return la; }
2020篇
1.二叉树, 从左到右输出第 k 层的节点;
- 思路:层次遍历, 队列(front)进行删除操作,(rear)进行插入操作
- 代码:
void K_Floor(BinNode *root, int k) { // 输出第 k 层的节点, 记 root 为第一层 int queue[maxsize]; // 队列结构 {node, n} ,n标记node所在层 int le; int front=0, rear=0; // 空时rear==front,不空时front指向第一个,rear指向最后一个的后一个位置 BinNode *p; queue[rear++].node = root; queue[front].n =1; while(front!=rear) { p = queue[front].node; le = queue[front].n; front = (front+1)%maxsize; if(le < k) //到 k-1 层为止, 恰好 k 层可以全部入队 { if(p->lchild != NULL) // 左孩子入队 { queue[rear].node = p->lchild; queue[rear].n = ++le; // 层数+1 rear = (rear+1)%maxsize; } if(p->rchild != NULL) // 右孩子入队 { queue[rear].node = p->rchild; queue[rear].n = ++le; // 层数+1 rear = (rear+1)%maxsize; } } if(le = k) print("%d ", p->data); } }
2.堆排序,删除p位置的元素,重排小根堆;所谓删除,应该是把p位置的数移到数组最后
- 小根堆排序
- 代码
void SheapDownAdjust(int *heap, ,int k, int h) { int i=k, j=2*k; int temp = heap[i] while(j<=h) { if(j< h && heap[j]>heap[j+1]) j++; //取较小的 if(temp>heap[j]) { heap[i] = heap[j]; i = j; j = 2*j; } else break; } heap[i] = temp; } void Delet_p(int *heap, int h, int p) { heap[n] = heap[p]; for(int i=n/2; i>0; i++) SheapDownAdjust(heap, i, h-1); }
2021篇
1.设带权有向图用邻接矩阵表示,判断图中是否存在从\(V_i\)到\(V_j\)的路径,并计算路径长度。要求给出邻阶矩阵的描述。
- 思路:从\(V_i\)开始深度遍历,如果在遍历中经过了\(V_j\)节点,则说明存在路径。
- 代码:
// typedef struct Graph { Adjlist[maxsize][maxsize]; // 邻接矩阵 int v,e; // 顶点数和边数 } Graph;
int MDFS1(Graph *G, int v, int j) { // 若存在路径,则返回长度。若不存在,则返回-1 static int visit[maxsize]; static int d=0; //记录遍历长度 static int result=-1; //记录结果长度 if(result!=-1) return result; //截断遍历, result不为-1时,说明已经找到路径 if(v == j) result = d; d++; visit[v] = 1; for(int k=0; k<maxsize; k++) if(G->Adjlist[v][k] && !visit[k]) MDFS1(G, k, j); d--; return result; }
2.中缀表达式转后缀表达式 可能出现的运算符 + - * / ^ ( )
- 思路:用两个栈,一个栈存放操作符, 一个存放后缀表达式结果
- 遇到数字直接入结果栈
- 遇到操作符
(
,直接入操作符栈,遇到)
则弹出上一个括号前的全部操作符入结果栈;遇到其他操作符时,若操作符栈为空,直接入栈;若不空,则需和栈顶符比较优先级,栈顶符优先级高或相同时将栈顶符弹出入结果栈,否则之间把现操作符压栈; - 代码
// 辅助函数 int isop(char c) { // c 是操作符返回 1, 否则返回 0; return (c=='+'||c=='-'||c=='*'||c=='/'||c=='^')?(1):(0); } int comop(char op1, char op2) { //如果 op2 操作符比 op1 操作符优先级高或相等,返回 1 ,否则返回 0; //if (op2=='(' && op1=='(') return 1; if (op2=='(') return 0; //遇到栈中的括号,返回 0 不允许计算 if (op1=='^') return 0; if (op1=='+' || op1=='-') return 1; if (op2=='*' || op2=='/' || op2=='^') return 1; return 0; } // void IntoRear(char *arr, int n) { char opstack[n]; //操作符栈 char restack[n]; //结果栈 int optop=-1, retop=-1; for(int i=0; i<n-1; i++) //字符串结尾有个空符 { if(arr[i]== '(' ) opstack[++optop] = arr[i]; else if(arr[i]== ')') { while(opstack[optop]!='(') restack[++retop] = opstack[optop--]; optop--; //跳过当前这个 '(' } else if(!isop(arr[i])) restack[++retop] = arr[i]; //数字直接入栈 else // 操作符时 { if(optop == -1) opstack[++optop] = arr[i]; //栈空,直接入栈 else { // comop(op1, op2), 当 op2 的优先级高于 op1 时返回 1, 否则返回 0; // opstack[optop] 栈顶的符号可能为括号,此时直接入操作符栈 if(opstack[optop]=='(' || comop(opstack[optop], arr[i])) opstack[++optop] = arr[i]; else { restack[++retop] = opstack[optop--]; opstack[++optop] = arr[i]; } } } } while(optop!=-1) restack[++retop] = opstack[optop--]; //弹出剩余的 for (int i = 0; i <= retop; i++) printf("%c ",restack[i]); }
2022篇
1.随机序列 \(A[a_0...a_{n-1}]\), 设计一个算法使\(a_i\)左边的元素都小于\(a_i\), \(a_i\)右边的元素都大于\(a_i\)。
- 快排划分思想
- 代码:
void QuickSort(int *nums, int n) { int L = 0, H = n-1; int temp = nums[L]; while(L<H) { while(L<H && nums[H]>temp) --H; nums[L] = nums[H]; //此处不能写成nums[L++] = nums[H],因为若本次循环没有比temp小的数,那么有H==L,经过nums[L++] = nums[H] 会导致 L 比 H 大1,指向错误的位置,但nums[L] = nums[H],会有已经确定的a[L]<temp重复判断一次,但结果是正确的。 while(L<H && nums[L]<temp) ++L; nums[H] = nums[L]; } nums[L] = temp; }
2.计算一颗树的外部带权路径长度WPl,即根节点到所有叶子的权值之和;
- 树的结构位
{lchild; rchild; weight}
- 思路:深度遍历;顺便记录路上权值,遇到叶子节点时累加一次
- 代码:
int WPL=0; int sum=0; void CalWPL(BTree *root) { if(root==NULL) return; sum += root->weight; //加权值 if(!root->lchild && !root->rchild) WPL += sum; CalWPL(root->lchild); CalWPL(root->rchild); sum -= root->weight; //还权值 }
总结篇:
- 以上内容为个人在2022考研复习期间整理出的内容,仅供参考;希望能给到后面考研的朋友一些帮助。