记录一次笔试编程题总结笔记

引言

本篇希望能记录当次笔试未清楚的几个题,解决心头之患,另外也为明年做准备。

双端队列的构造

此题好像是什么地方的原题,我在搜索上找到一篇全英的转载,所以我主要参考了最后第一篇以及第二篇我引用的那篇思路,结合自己的理解对这题进行了复盘。过程如下:

题目描述

6-7 Deque(25 point(s))

A “deque” is a data structure consisting of a list of items, on which the following operations are possible:

  • Push(X,D): Insert item X on the front end of deque D.
  • Pop(D): Remove the front item from deque D and return it.
  • Inject(X,D): Insert item X on the rear end of deque D.
  • Eject(D): Remove the rear item from deque D and return it.

Write routines to support the deque that take O(1) time per operation.

函数格式:

Deque CreateDeque();
int Push( ElementType X, Deque D );
ElementType Pop( Deque D );
int Inject( ElementType X, Deque D );
ElementType Eject( Deque D );

算法思想

这里的双端队列是由一个带有标题的双向链表实现的。Front并分别Rear指向双端队列的两端。Front始终指向标题。当双端队列为空Front和Rear:都指向同一个虚拟header。补充:Push和Inject应该返回1,如果操作可以成功完成,或0,如果失败。如果双端队列为空,Pop且Eject必须返回ERROR它是由judge程序中定义。
在这里插入图片描述

定义结构体:

typedef struct Node *PtrToNode;
struct Node {
    
    
    ElementType Element;
    PtrToNode Next, Last;
};
typedef struct DequeRecord *Deque;
struct DequeRecord {
    
    
    PtrToNode Front, Rear;
};

上面的图同样是引用参考文献中的一篇知乎帖子,与结构体基本对应,我也就没自己用Visio画了。

判断程序

#include <stdio.h>
#include <stdlib.h>
 
#define ElementType int
#define ERROR 1e5
typedef enum {
    
     push, pop, inject, eject, end } Operation;
 
typedef struct Node *PtrToNode;
struct Node {
    
    
    ElementType Element;
    PtrToNode Next, Last;
};
typedef struct DequeRecord *Deque;
struct DequeRecord {
    
    
    PtrToNode Front, Rear;
};
Deque CreateDeque();
int Push( ElementType X, Deque D );
ElementType Pop( Deque D );
int Inject( ElementType X, Deque D );
ElementType Eject( Deque D );
 
Operation GetOp();          /* details omitted */
void PrintDeque( Deque D ); /* details omitted */
 
int main()
{
    
    
    ElementType X;
    Deque D;
    int done = 0;
 
    D = CreateDeque();
    while (!done) {
    
    
        switch(GetOp()) {
    
    
        case push: 
            scanf("%d", &X);
            if (!Push(X, D)) printf("Memory is Full!\n");
            break;
        case pop:
            X = Pop(D);
            if ( X==ERROR ) printf("Deque is Empty!\n");
            break;
        case inject: 
            scanf("%d", &X);
            if (!Inject(X, D)) printf("Memory is Full!\n");
            break;
        case eject:
            X = Eject(D);
            if ( X==ERROR ) printf("Deque is Empty!\n");
            break;
        case end:
            PrintDeque(D);
            done = 1;
            break;
        }
    }
    return 0;
}
 
/* Your function will be put here */

样本输入为:

Pop
Inject 1
Pop
Eject
Push 1
Push 2
Eject
Inject 3
End

输出实例为:

Deque is Empty!
Deque is Empty!
Inside Deque: 2 3

双端队列构造程序


//模型:front不动,每次插入的时候是在front和它下一个节点之间插入,而rear每次都是移动的,每次都是插到后面,并且rear每次都要
Deque CreateDeque(){
    
    //移动指向最后一个上。
    Deque p;
    p = (Deque)malloc(sizeof(struct DequeRecord));//创建头尾指针
    p->Front = (PtrToNode)malloc(sizeof(struct Node));//在头上先创建一个虚的节点
    p->Front->Last = NULL;//前一个为null说明队列左边没东西
    p->Rear = p->Front;//左右相同指向同一个
    p->Rear->Next = NULL;//后一个为null说明右边没有东西
    return p;
}
int Push( ElementType X, Deque D ){
    
    //每次在front和下一个节点之间插入
    struct Node* temp;
    temp = (struct Node*)malloc(sizeof(struct Node));
    if(!temp)return 0;//内存满了,申请失败
    temp->Element = X;//赋值
    if(D->Front==D->Rear){
    
    //如果是个空双端队列
        D->Front->Next = temp;//插入第一个点
        temp->Last = D->Front;//回指向front
        D->Rear = temp;//rear指针移动指向第一个新插入的,代表从左边进入的第一个最靠近右边
        temp->Next = NULL;//第一次在左边进入的第一个点下一个将不再指向任何东西,因为每次
        return 1;         //都在前一个插入,第一个点会越来越远
    }
    //一般情况
    temp->Next = D->Front->Next;//新节点下一个指向原来front指向的下一个
    temp->Last = D->Front;//新节点的前一个指向fronr
    D->Front->Next->Last = temp;//front原来所值元素的前一个指向新的节点
    D->Front->Next = temp;//front的下一个指向新节点
    return 1;
}
ElementType Pop( Deque D ){
    
    
    if(D->Front==D->Rear)
        return ERROR;//如果空队列返回错误
    int temp = D->Front->Next->Element;//保存pop出的值
    struct Node* t = D->Front->Next;//保存它是为了最后把它内存释放掉
    if(D->Front->Next==D->Rear){
    
    //队列中只有一个元素的时候
        D->Rear = D->Front;//删除后rear前移使得rear和front相等
        D->Rear->Next = NULL;//虚节点指向空
        free(t);
        return temp;
    }
    //一般情况
    D->Front->Next->Next->Last = D->Front;//我们要删除front的前一个所以删除之后front前一个的前一个的Last应该指回Front
    D->Front->Next = D->Front->Next->Next;//同理front下一个应该是原来没删前下一个的下一个
    free(t);
    return temp;
}
int Inject( ElementType X, Deque D ){
    
    //从右边插入就直接插到后面,然后rear后移
    struct Node* temp = (struct Node*)malloc(sizeof(struct Node));
    if(!temp)return 0;
    temp->Element = X;
    if(D->Front==D->Rear){
    
    //空双端队列
        D->Front->Next = temp;
        temp->Last = D->Front;
        D->Rear = temp;
        return 1;//和push的一样
    }
    //一般情况
    D->Rear->Next = temp;//rear下一个等于新节点
    temp->Last = D->Rear;//新节点前一个等于现在rear指的点
    temp->Next = NULL;//temp的下一个指向空
    D->Rear = temp;//rear右移到当前点
    return 1;
}
ElementType Eject( Deque D ){
    
    
    if(D->Front==D->Rear){
    
    //空队列返回错误
        return ERROR;
    }
    int temp = D->Rear->Element;//保存值
    struct Node* t = D->Rear;
    D->Rear = D->Rear->Last;//删掉节点rear指回前一个节点
    D->Rear->Next = NULL;//现在节点为最后一个,所以指向空
    free(t);
    return temp;
 
}

注释很详细,但要在临场手写这么多代码,不得不说需要很强的编码能力以及清晰的思路。

笛卡尔树条件判断

笛卡尔树定义(来源:百度百科)

笛卡尔树是一种特定的二叉树数据结构,可由数列构造,在范围最值查询、范围top k查询(range top k queries)等问题上有广泛应用。它具有堆的有序性,中序遍历可以输出原数列。笛卡尔树结构由Vuillmin(1980)在解决范围搜索的几何数据结构问题时提出。从数列中构造一棵笛卡尔树可以线性时间完成,需要采用基于栈的算法来找到在该数列中的所有最近小数。

无相同元素的数列构造出的笛卡尔树具有下列性质:

  1. 结点一一对应于数列元素。即数列中的每个元素都对应于树中某个唯一结点,树结点也对应于数列中的某个唯一元素
  2. 中序遍历(in-order traverse)笛卡尔树即可得到原数列。即任意树结点的左子树结点所对应的数列元素下标比该结点所对应元素的下标小,右子树结点所对应数列元素下标比该结点所对应元素下标大。
  3. 树结构存在堆序性质,即任意树结点所对应数值大/小于其左、右子树内任意结点对应数值

题目描述

笛卡尔树是一种特殊的二叉树,其结点包含两个关键字K1和K2。首先笛卡尔树是关于K1的二叉搜索树,即结点左子树的所有K1值都比该结点的K1值小,右子树则大。其次所有结点的K2关键字满足优先队列(不妨设为最小堆)的顺序要求,即该结点的K2值比其子树中所有结点的K2值小。给定一棵二叉树,请判断该树是否笛卡尔树。

输入格式:
输入首先给出正整数N(≤1000),为树中结点的个数。随后N行,每行给出一个结点的信息,包括:结点的K1值、K2值、左孩子结点编号、右孩子结点编号。设结点从0~(N-1)顺序编号。若某结点不存在孩子结点,则该位置给出−1。

输出格式:
输出YES如果该树是一棵笛卡尔树;否则输出NO。

输入样例1:

6
8 27 5 1
9 40 -1 -1
10 20 0 3
12 21 -1 4
15 22 -1 -1
5 35 -1 -1

输出样例1:

YES

输入样例2:

6
8 27 5 1
9 40 -1 -1
10 20 0 3
12 11 -1 4
15 22 -1 -1
50 35 -1 -1

输出样例2:

NO

算法思想

首先笛卡尔树是关于K1的二叉搜索树:
左子树满足二叉搜索树,右子树满足二叉搜索树,左子树的最大K1值小于根结点的K1值,右子树的最小K1值大于根结点的K1值。

其次所有结点的K2关键字满足优先队列的顺序要求:
左子树满足优先队列的顺序要求,右子树满足优先队列的顺序要求,且根结点的K2值比左子树根结点的K2值和右子树根结点的K2值都小。
在这里插入图片描述
(图源自维基百科)
在这里插入图片描述

判断笛卡尔树代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct node
{
    
    
 int K1;//K1关键字
 int K2;//K2关键字
 int left;//左孩子下标
 int right;//右孩子下标
};
typedef struct node * Bintree;
int JudgeK1(int n);//判断是否是关于K1的二叉搜索树
int JudgeK2(int n);//判断所有结点的K2关键字是否满足优先队列的顺序要求
Bintree B;//结构体数组B
int main()
{
    
    
 int N;
 scanf("%d", &N);
 if (N == 1) {
    
     printf("YES\n"); }
 else
 {
    
    
  B = (Bintree)malloc(N * sizeof(struct node));
  int i; int root;
  //用数组t寻找根结点。出现过的下标i对应的t[i]设为1,最后t[j]==0的下标对应根结点
  int * t;
  t = (int *)malloc(N * sizeof(int));
  for (i = 0; i < N; i++)
  {
    
    
   t[i] = 0;
  }
  for (i = 0; i < N; i++)
  {
    
    
   scanf("%d %d %d %d", &(B[i].K1), &(B[i].K2), &(B[i].left), &(B[i].right));
   if (B[i].left != -1) {
    
     t[B[i].left] = 1; }
   if (B[i].right != -1) {
    
     t[B[i].right] = 1; }
  }
  for (i = 0; i < N; i++)
  {
    
    
   if (t[i] == 0) {
    
     break; }
  }
  root = i;
  int ret1 = JudgeK1(root);
  int ret2 = JudgeK2(root);
  if (ret1 == 1 && ret2 == 1)
  {
    
    
   printf("YES\n");
  }
  else
  {
    
    
   printf("NO\n");
  }
 }
 return 0;
}
//判断是否是关于K1的二叉搜索树,迭代
//输入参数为根结点下标
int JudgeK1(int n)
{
    
    
 int leftmax=-100000, rightmin=100000;
 if (n == -1) {
    
     return 1; }
 else
 {
    
    
  int ret1, ret2;
  ret1 = JudgeK1(B[n].left);//判断左子树
  ret2 = JudgeK1(B[n].right);//判断右子树
  int index;
  //寻找左子树的最大K1值leftmax
  index = B[n].left;
  if (index != -1)
  {
    
    
   while (B[index].right != -1)
   {
    
    
    index = B[index].right;
   }
   leftmax = B[index].K1;
  }
  //寻找右子树的最小K1值rightmin
  index = B[n].right;
  if (index != -1)
  {
    
    
   while (B[index].left != -1)
   {
    
    
    index = B[index].left;
   }
   rightmin = B[index].K1;
  }
  if (ret1&&ret2&&leftmax < B[n].K1&&B[n].K1 < rightmin) {
    
     return 1; }
  else {
    
     return 0; }
 }
}
//判断所有结点的K2关键字是否满足优先队列的顺序要求,迭代
//输入参数为根结点下标
int JudgeK2(int n)
{
    
    
 if (n == -1) {
    
     return 1; }
 else
 {
    
    
  int ret1, ret2;
  ret1 = JudgeK2(B[n].left);//判断左子树
  ret2 = JudgeK2(B[n].right);//判断右子树
  //且根结点的K2值比左子树根结点的K2值和右子树根结点的K2值都小
  if (B[n].left != -1 && B[n].right != -1)
  {
    
    
   if (ret1&&ret2&&B[B[n].left].K2 > B[n].K2&&B[B[n].right].K2 > B[n].K2) {
    
     return 1; }
   else {
    
     return 0; }
  }
  else if (B[n].left != -1)
  {
    
    
   if (ret1&&ret2&&B[B[n].left].K2 > B[n].K2) {
    
     return 1; }
   else {
    
     return 0; }
  }
  else if (B[n].right != -1)
  {
    
    
   if (ret1&&ret2&B[B[n].right].K2 > B[n].K2) {
    
     return 1; }
   else {
    
     return 0; }
  }
  else
  {
    
    
   if (ret1&&ret2) {
    
     return 1; }
   else {
    
     return 0; }
  }
 }
}

修理牧场

哈夫曼定义

在这里插入图片描述
(来源于王道数据结构)

题目描述

农夫要修理牧场的一段栅栏,他测量了栅栏,发现需要N块木头,每块木头长度为整数L​i​​个长度单位,于是他购买了一条很长的、能锯成N块的木头,即该木头的长度是L​i​​的总和。

但是农夫自己没有锯子,请人锯木的酬金跟这段木头的长度成正比。为简单起见,不妨就设酬金等于所锯木头的长度。例如,要将长度为20的木头锯成长度为8、7和5的三段,第一次锯木头花费20,将木头锯成12和8;第二次锯木头花费12,将长度为12的木头锯成7和5,总花费为32。如果第一次将木头锯成15和5,则第二次锯木头花费15,总花费为35(大于32)。

请编写程序帮助农夫计算将木头锯成N块的最少花费。

输入格式:
输入首先给出正整数N(≤10^4),表示要将木头锯成N块。第二行给出N个正整数(≤50),表示每段木块的长度。

输出格式:
输出一个整数,即将木头锯成N块的最少花费。

输入样例:

8
4 5 1 2 1 3 1 1

输出样例:

49

算法思想

我们将木头的长度看成树的节点,每切割一次木头就相当与把节点分为两个子节点,每个节点的权值就相当于分的的木头长度。那么最后这棵树的总权值就相当于切割的代价。问题就变成求这棵树的最小权值,这不就是Hoffman树的作用嘛。所以这道题目就变成了求一棵Hoffman树的权值。

算法求解

#include<stdio.h>

#define swap(a,b) a=a^b,b=a^b,a=a^b

int tail=0;     //堆的最后一个元素的下一个位置

void Insert(int *length,int temp);
int GetHeap(int *length);
int Solve(int *length);

int main(void)
{
    
    
    int N;
    int i;
    scanf("%d",&N);
    int length[N];
    int temp;

    for(i=0;i<N;i++){
    
    
      scanf("%d",&temp);
      Insert(length,temp);      //构造一个小顶堆
    }

    printf("%d",Solve(length));

    return 0;
}

void Insert(int *length,int temp)
{
    
    
    int son,father;

    son=tail;
    length[tail++]=temp;
    father=(son+(son&1))/2-1;

    if(!son)
      return ;

    while(length[son]<length[father]&&father>=0){
    
    
      swap(length[son],length[father]);    //交换两节点位置
      son=father;
      father=(father+(father&1))/2-1;
    }

    return ;
}

int GetHeap(int *length)
{
    
    
    int result=length[0];
    int father=0;
    int son=1;

    length[0]=length[--tail];

    while(son<tail){
    
    
      if(length[son]>length[son+1])
        son++;                      //如果左孩子节点的数值更大
      if(length[father]>length[son]){
    
    
        swap(length[father],length[son]);
        father=son;
        son=2*son+1;
      }
      else
        break;
    }

    return result;
}

int Solve(int *length)
{
    
    
    int min1,min2;
    int result=0;

    if(tail==1)
      return 0;

    while(1!=tail){
    
    
      min1=GetHeap(length);
      min2=GetHeap(length);   //找打最小的两块板子
      result+=min1+min2;      //将其合为一块板子
      Insert(length,min1+min2);     //继续排序
    }

    return result;
}



Reference

[1]. 6-7 Deque(25点)

[2]. 抛砖引玉 | 图解双端队列Deque

[3]. https://oi-wiki.net/ds/cartesian-tree/

[4]. https://www.luogu.com.cn/problem/P5854

[5]. 数据结构PTA习题:进阶实验4-3.4 笛卡尔树 (25分)

[6]. PTA 数据结构与算法 7-29 修理牧场

猜你喜欢

转载自blog.csdn.net/submarineas/article/details/122232456