802数据结构(重庆邮电大学)考纲自查(最终篇)

1.已知二叉树用二叉链表存储,试编写一函数实现计算该树的高度。定义必要的数据结构(期末P10T5)

typedef struct BiTNode{
    Elemtype data;
    struct BiTNode *lchild, *rchild;
}BiTNode;

int BiTreeDepth(BiTNode* T){
    if(!T) return 0;
    else{
        int l = BiTreeDepth(T->lchild);
        int r = BiTreeDepth(T->rchild);
        return l>r? l+1: r+1;
    }
}

2.A是个有M个数据的队列,另有N个数据的有序序列B,某程序将数据从队列A中取出,使用二分法查找该数据在B中的位置并输出。试分析该程序的时间复杂度并分析步骤(期末P13T2)

首先,整个算法有两大步骤,分别为①从队列A中取数和②在序列B中二分查找取出的数。下面将逐个进行分析。

第一,从队列A中取数,该操作本身时间复杂度为O(1),每次取出数据后要对其在序列B中的位置进行二分查找,共需要查找M个数据。

第二,考虑单次查找的情况。二分查找每次查找会通过 low, mid, high 三个整型变量将待查找子序列分为两部分,且两部分长度之差最多为一。因此对于二分查找构造的查找判定树深度不超过⌈log2(N+1)⌉,即单次查找的时间复杂度为O(log2N)。

综上所述,每次从队列A中取出一个元素的时间复杂度为O(1),对该元素进行二分查找的时间复杂度为O(log2N),共进行M次这样的操作,所以总时间复杂度为O(Mlog2N)。

3.对BST的中序遍历会得到从小到大递增的序列,写一函数,变换BST,使得对新BST的中序遍历得到从大到小递减的序列。并描述如何在该树中找最大元素(期末P14T1)

//有关数据结构描述如下
struct BTNode{
    int data;
    BTNode* lchild;
    BTNode* rchild;
};

//
void BstInvert(BTNode* T){
//    if(T==NULL) return;
//    if(T->lchild==NULL && T->rchild==NULL) return;
//    BTNode *l = T->lchild, *r = T->rchild;
//    T->lchild = r, T->rchild = l;
//    BstInvert(T->lchild);
//    BstInvert(T->rchild);
}

void Reverse(BiTree T){//@20221112
    if(T==NULL) return;//递归退出条件:没有子树待调整
    BiTNode* a = T->lchild;//指针a指向左子树
    Reverse(a);//递归调整左子树
    BiTNode* b = T->rchild;//指针b指向右子树
    Reverse(b);//递归调整右子树
//左右子树调整完整后
    T->lchild = b;//原本的右子树连到父结点左边
    T->rchild = a;//原本的左子树连到父结点右边
}

以上操作对BST进行变换后,中序遍历会得到一个从大到小递减的序列,此时左子树根结点值>根结点值>右子树根结点值。即中序遍历的第一个结点即为值最大的结点。

实现方法为,从根结点开始,若有左子树,则往左子树;若左子树为空,则取最后一个遍历到的分支结点值。

4.有一个单链表L(至少有一个结点),其头结点指针为L,编写一个过程将L逆置,要求逆转在原链表上进行(真题2012)

void Reverse(LinkList L){
    LNode *p, *q;
    p = L->next;//p标记链表表头
    L->next = NULL;//头指针指空,等待插入
    while(q){//当前插入结点不为空循环
        q = p;//取原链表第一个结点
        p = p->next;//原链表表头后移一位
        q->next = L->next;//头插法
        L->next = q;
    }
}

原地逆置链表的思路为,用一个新的头结点p标记原链表,将L所指链表置空,随后不断地从p所指链表中取出表头结点,将其插入到L所指链表的表头。这样,原先在p中靠后的结点就会被转移到L靠前的位置。

5. 某顺序表中的数据序列为降序,设计一算法变为升序(真题2013)

思路1:准备一个新的数组B,原数组A从后向前依次取出元素从头插入数组B。

思路2:准备两个整型变量low和high,分别指向顺序待排序子列第一个元素和最后一个元素,每次循环交换low和high所指元素,并执行low++,high--

void Reverse(int A[], int n){//思路1
    int B[], i, j;
    for(i=n-1, j=0; i>=0, j<n; i--, j++)
        B[j] = A[i];
}

void Reverse(int A[], int n){//思路2
    int low=0, high = n-1;
    while(low<high){//交换待排序子列首尾元素
        int temp = A[low];
        A[low] = A[high];
        A[high] = temp;
        low++, high--;
    }
}

关于性能,思路1中,很明显,时空复杂度都是O(n)。

思路2中,空间复杂度为O(1),时间复杂度基于while循环,循环次数不超过n/2,实际上是n/2向下取整,时间复杂度也是O(n),但是比思路一好些。 

6.现有某比赛活动中某选手的n个评分结果(百分之且都是60以上的整数),并且已按升序排列。为了分析相关规律,现需分析评分的k-均值直方图(以当前分数为中位数(位置在中间),计算最邻近的k个分数平均值并取为整数,然后统计分数的频率得到。若当前得分左边或右边数据不足时直接忽略,不计入统计数据),请设计一尽量快速的算法,打印直方图当前的统计结果(真题2014)

思路:俺也不会。

翻译一下题目就是有一长度为n且升序的数组。然后给一k,对于数组中的第i个元素,要满足它左右两边加上它自己能有k个元素,也就是说左右至少各有k/2个元素(当k取偶数时,不妨左边取k/2个,右边取k/2-1个,再算上第i个共k/2个;当k取奇数时,左边取k/2(向下取整),右边取k/2个,再算上第i个共k/2个)。这就要求第i个元素左边必然要有k/2个元素,所以 i 的下限是k/2+1;同时也要求i右边也有k/2个元素,所以i的上限是n-k/2-1。然后就对这 n-k 个元素挨个求平均值。求出平均之后,还要对这些平均值统计出现频率,最终输出这些频率。

int L[n]; //L用来存放每个元素周围的k个元素和
void k-frequency(int data[], int n, int k){
    //data[] 存放待统计数据,n为表长
    int m=0;
    for(int i=k/2+1; i<n-k/2; i++){//计算合法范围内每个i周围k个元素和
        int res=0;
        for(int j=0; j<k; j++){
            res+=data[j];
        }
        L[m++]=res;//将和存入数组
    }
    for(int i=0; i<m; i++){
        int f=0;//f用于统计出现频率
        if(L[i]!=0){//如果当前元素不是0
            print("%d:",L[i]/k);//按照“平均值:频率”的格式输出“平均值:”
            f++;//频率+1
            for(int j=i+1; j<m; j++){//遍历L统计L[i]出现频率
                if(L[j]==L[i]) f++;//如果出现则f+1
            }
            print("%d\n", f);//按照“平均值:频率”的格式输出“频率”
        }
    }
}

7.重排数组,负值在前非负值在后(真题2015)

思路:考虑以0为基准进行一趟快排,复杂度O(log2n)

void Qsort2015(int A[], int n){
    int low = 0, high = n-1;//低位和高位指针
    while(low<high){//循环退出条件
        while(low<high&&A[low]<0) low++;//找到左边开始第一个非负的
        while(low<high&&A[high]>=0) high--;//找到右边开始第一个负的
        int temp = A[low];//交换
        A[low] = A[high];
        A[high] = temp;
    }//直到low>=high,左右子列都已处理完毕
}

8.判断BST(真题2015)  

思路:BST的中序遍历是递增的,因此可以设置全局变量保存上一层递归的结点数据,在这一层递归中比较,如果这一层更大,则更新全局变量进入下一层递归;如果这一层相同或小,则检查失败,不是BST。整个思路和中序遍历类似。递归深度为树高O(logn),时间复杂度为结点数量O(n)。

int max=-99999;//设置初始最小值
int falseflag=0;//设置false指示位以递归退出函数
bool judge(BiTree T){
    if(T==NULL) return true;//若遍历到叶子结点下一层,认为成功返回
    if(falseflag==1) return false;//若之前已返回false,则递归退出函数
    judge(T->lchild);//递归判断左子树
    if(T->data>max) max=T->data;//若当前根结点更大,则更新max值
    else{//否则根结点小于左子树值,则不是BST
        falseflag=1;//false标志位置一
        return false;//返回false
    }
    judge(T->rchild);//递归判断右子树
    return true;//该结点所有子树遍历完后返回true回到上层递归
}

9.已知中序和前序序列,构造二叉树并返回根结点,时间复杂度尽可能低(佬の预测)

思路:对于这种变态的题目,不知道从哪开始可以从手算入手。如果已知前序序列 char pre[] 和 char in[],易知两字符串长度相同,且为树中结点总数。手动构造的时候,需要先看pre[0],也就是前序的第一个结点,这个结点必定是整棵树的根结点,记这个结点为 R,接下来要做的是去 in[] 中,找到 R 所在位置,假如是 k,那么整个 in[] 会被分成 (0...k-1) 和 (k+1...n-1) 两段,且左边是 R 的左子树,右边是 R 的右子树。到这里,问题的规模已经从 n 降到了 (n-1)/2,接下来就需要对 R 的左子树和右子树再执行和上述相同的过程,但是如何执行仍然是个问题。不如将问题再细化一下:当使用 k 将 in[] 分成两段后(假如叫 in1[] 和 in2[]),需要接着遍历 pre[],找到 pre[1],因为按照先序遍历的规则,先遍历根结点,然后是左子树,最后是右子树,这就意味着 R 后面的那个结点(假如叫 S),一定是 R 左子树的根结点。那么同样的,可以找到 S 在 in1[] 中的位置(假如为 l),将 in1[] 分为 (0...l-1) 和 (l+1...k-1) 两段。接下来呢?按照前面的分析,pre[2] 应当是 S 的左子树的根结点,以此类推。直到什么时候退出递归?回顾一下前面的分析过程,首先第一次处理,问题规模为 n,在 in[] 中找到 R,然后用 k 将 in[] 划分成两段;接着第二次处理,问题规模为 n/2,在 in1[](0...k-1)找到 S,然后用 l 将 in1[] 划分成两段;接着第三次处理,问题规模为 n/4,应当是在 in1[] 的左半段接着找 S 的左孩子。当 pre[i] 在左半段中找不到的时候,则需要退出这一层递归,回到上一层递归,然后在右半段中找。至此思路便清晰了,尝试分析一下性能。

空间复杂度:显然,递归层数不会超过树高,应为O(log2n)。

时间复杂度:首先大体上有个对于 pre[] 的遍历过程,从 0 到 n-1,时间为 O(n)。其次是递归查找过程,第一层递归,问题规模为 n,第二层递归,问题规模虽然减半,但是从第一层进第二层递归查找左子树是 n/2,从第三层回第二层递归查找右子树还是 n/2,所以第二层总体规模并未减半,仍为 n,后序层数同理,所以每层递归的查找时间复杂度都为 O(n)。总时间复杂度是 O(nlog2n) 还是 O(n^2)?注意,查找的 O(n) 是对应于每层的,事实上如果以 pre 的每个元素为线索分析查找问题规模,比较复杂,可以想一想,第一个元素是 n,第二个元素是 n/2,第三个元素是 n/4,第四个是 n/8,假如此时退回了,第五个是 n/4,第六个是 n/2(开始去根结点的右子树),第七个是 n/4 ……很难分析,而采用递归每层一个 O(n) 的思想,共 O(nlog2n)。应该没有比这个更低的复杂度了。

int k=0;//控制pre遍历
BiTree buildtree(char pre[], char in[], int low, int high){
    char root = pre[k++];//每次递归开始时,k指向pre下一个元素
    int i;
    for(i=low; i<high; i++){
        if(in[i]==root) break;
    }//找到pre[k]在中序中的位置
    if(i>high){//如果没找到
        k--;//k回退一位,因为这趟递归无效
        return NULL;//返回NULL
    }
    BiTNode *l = buildtree(pre, in, low, i-1);//递归返回左子树根结点
    BiTNode *r = buildtree(pre, in, i+1, high);//递归返回右子树根结点
    BiTNode *R = (BiTNode *)malloc(sizeof(BiTNode));//新建根结点
    R->data = root;//根结点值为pre[k]
    R->lchild = l;//左子树根结点连到R左孩子
    R->rchild = r;//右子树根结点连到R右孩子
    return R;//返回根结点
}

10.设计一个算法,将有n个元素的数组A中的元素A[0]至A[n-1]循环右移k位,并要求只用一个元素大小的附加存储,元素移动或交换次数为O(n)(真题2016)

思路:背诵:对前n-k个元素逆置,再对后k个元素逆置,最后对全部n个元素逆置。例如:123456,k=4,先对1234逆置得4321,再对56逆置得65,最后对432165逆置得561234。

void REVERSE(int A[], int low, int high){//逆置 low 到 high
    int temp;
    for(int i=0; i<(high-low+1)/2; i++){//i控制增量,只需循环low到high长度的一半
        temp = A[high-i];
        A[high-i] = A[low+i];
        A[low+i] = temp;
    }
}

void movek(int A[], int n, int k){
    REVERSE(A, 0, n-k-1);//逆置前n-k个
    REVERSE(A, n-k, n-1);//逆置后k个
    REVERSE(A, 0, n-1);//逆置全部n个
}

11.BST查找(真题2016)

思路:BST树的特性,左子树根结点值<根结点值<右子树根结点值,所以可以通过比较根结点和待查找关键字的大小确定是去左子树查找还是去右子树查找。最终当查找到空的时候查找失败。有两种写法,循环或递归。

BiTNode *BSTsearch1(BiTree T, int key){//递归写法
    if(T->data==key || T==NULL) return T;//找到或者为空时,开始返回
    if(T->data<key) return BSTsearch1(T->lchild, key);//若关键字小于根结点值,则在左子树中找
    else if(T->data>key) return BSTsearch1(T->rchild, key);//若关键字大于根结点值,则在右子树中找
}

BiTNode *BSTsearch2(BiTree T, int key){//循环写法
    while(T->data!=key && T!=NULL){//如果没找到并且没找完则循环
        if(T->data<key) T=T->lchild;//若关键字小于根结点值,则在左子树中找
        else if(T->data>key) T=T->rchild;//若关键字大于根结点值,则在右子树中找
    }
    return T;//返回最终根结点(可能为NULL)
}

12.判断AVL(真题2017)

思路:对于AVL中的任意一个结点,其左右子树高度之差不大于1,即左子树高度-右子树高度的范围在-1到1之间。因此,递归对每个结点进行判断,直到没有子树时返回NULL,若有一个不平衡则直接返回false。和第7题的判断BST比较像。

int gethigh(BiTree T){//计算当前树高
    if(T==NULL) return 0;//空树记为0
    int hl = gethigh(T->lchild);//计算左子树高度
    int hr = gethigh(T->rchild);//计算右子树高度
    return hl>hr?hl+1:hr+1;//去较大高度,再加上根结点
}

int falseflag=0;
bool judgeAVL(BiTree T){
    if(falseflag==1) return false;
    if(T==NULL) return true;//到最低层仍是AVL则返回true
    int b = gethigh(T->lchild) - gethigh(T->rchild);//计算当前结点平衡因子
    if(b<-1||b->1){//判断当前结点是否平衡
        falseflag = 1;
        return false;
    }
    judgeAVL(T->lchild);//判断左子树是否平衡
    judgeAVL(T->rchild);//判断右子树是否平衡
    return true;
}

13.设有大小不等的n个数据组,其数据总量为m,顺序存放在空间区D内,每个元素占一个存储单元,数据组的首地址由数组S给出(如图所示),试编写将新数据x插入到第i个数据组的末尾且属于第i个数据组的算法,插入后,空间区D和数组S的相互关系仍保持正确(真题2019)

思路:D是一个用来存放某种数据类型(Elemtype)的数据的数组,其实本质上是一维的。但是D中由划分出n个数据组,每一段的起始地址(下标)由S给出。例如,第i个数据组的第一个数据元素,用D来表示就是D[S[i]]。如此一来,想在第i个数据组的末尾插入元素x,因为不知道第i个数据组的长度,所以换个思路就是在第i+1个数据组的起始位置之前插入元素x。为此,需要将D中从S[i+1]这个下标到m-1的所有元素后移一位。同时还需要D和S相互关系保持正确,就需要将S中i+1到n的所有起始位置+1(因为前面在D中后移了一位)。说到这代码就不难了。

void insertx(int S[], Elemtype D[], int i, Elemtype x){
    if(i<1 || i>n) exit(0);//合法插入范围为S中的 1~n
    if(i==n) D[m]=x;//如果在最后一个数据组尾端插入,则对应D中的D[m]
    else{//其他位置
        for(int j=m-1; j>=S[i+1]; j--){//将S[i+1]到m-1位置的所有元素后移一位
            D[j+1] = D[j];
        }
        D[S[i+1]] = x;//把x插入S[i+1]的位置
        for(int j=i+1; j<=n; j++){//第i+1及以后的每个数据组起始地址后移一位
            S[j]++;
        }
    }
    m++;//D总长+1
}

14.用平均值为轴快排(待排序数据存在数组R[]中,数组最小下标为S,数组最大下标为T)(真题2019)

思路:每轮排序以平均值为基准,从左边找到大于平均值的元素和右边小于平均值的元素互换。

int partition(int R[], int S, int T){
    int l=T-S+1, avg=0;
    for(int i=S; i<=T; i++){
        avg+=R[i];
    }//求和
    avg/=l;//求平均值
    int i=S; j=T;
    int temp = R[i];
    while(i<j){//交换
        while(R[j]>avg&&i<j) j--;
        R[i]=R[j];
        while(R[i]<avg&&i<j) i++;
        R[j]=R[i];
    }
    R[i] = temp;
    return i;
}

int avgsort(int R[], int S, int T){
    int k = partition(R, S, T);
    avgsort(R, S, k);//不一定有元素确定位置,所以不是k-1
    avgsort(R, k+1, T);
}

15.设带头结点的双向循环链表表示的线性表为L=(a1,a2,a3,...,an)。写一算法,O(n),将L改造为L=(a1,a3,...,an,...,a4,a2)(真题2020)

思路:光看题目似乎没有思路,不妨任意写一个链表 A{1,2,3,4,5,6,7,8,9,10},改造的目标链表为 B{1,3,5,7,9,10,8,6,4,2};或 C{1,2,3,4,5,6,7,8,9} 改造为 D{1,3,5,7,9,8,6,4,2}。重点是找规律,直接筛选出奇数和偶数元素显然在O(n)的时间内不现实,不难注意到奇数下标的元素相对位置其实没有改变,改变的只有偶数下标的元素位置,而且变成了逆序。所以考虑从头到尾遍历,每次把偶数下标元素在最后一个偶数下标元素之后执行“头插法”。例如在A到C的改造过程中,A={1,2,3,4,5,6,7,8,9,10}取出2插到10后面->{1,3,4,5,6,7,8,9,10,2}取出4插到10后面->{1,3,5,6,7,8,9,10,4,2}取出6插到10后面->{1,3,5,7,8,9,10,6,4,2}取出8插到10后面->{1,3,5,7,9,10,8,6,4,2}=C,逻辑正确。且遍历链表的次数为n/2,满足O(n)的要求。

typedef struct LNode{
    int data;
    LNode *prior, *next;
}LNode, *Linklist;

void transform(Linklist L){
    int cnt=1;
    LNode *p=L->next, *tail=L->prior;//带头结点
    while(p!=tail){//如果遍历到an则循环终止
        if(cnt%2==0){//如果遍历到第偶数个元素
            LNode *q=p->next;//保存p->next的信息
            p->prior->next=p->next;//在原位置删除p
            p->next->prior=p->prior;
            p->next=tail->next;//在an后插入p
            p->prior=tail;
            tail->next->prior=p;
            tail->next=p;
            p=q;//p回到原本的下一个位置
        }
        else p=p->next;//如果遍历到第奇数个元素,直接遍历下一个
        cnt++;//记录当前所在位置
    }
}

16.ODER公司是一个专门为人们提供排序服务的公司。他们的工作是通过一系列移动,将某些物品按顺序摆好。他们的服务是通过工作量来计算的,即移动东西的次数。所以,在工作前必须考察工作量,以便向用户提出收费价格。用户并不知道精确的移动次数,实质上,大多数人是凭感觉来认定这一列物品的混乱程度。根据ODER公司的经验,人们一般是根据“逆序对”的数目多少来称呼这一序列的混乱程度。

假设我们将序列中第i件物品的参数定义为Ai,那么排序就是指将A1,Ai,An按从小到大的顺序排序。若i<j且Ai>Aj,则<i,j>就为一个“逆序对”。

例如数组(3,1,4,5,2)的逆序对有<3,1>,<3,2>,<4,2>,<5,2>,共4个。

现在,ORDER公司请你写一个程序,在尽量短的时间内,统计出逆序对的数目,程序输入n,A1,...Ai,...,An,1<=n<=30000,Ai为小于200的正整数。输出序列的逆序数(真题2020)

思路:首先硬算的复杂度是O(n^2),应该有分,但不多。盯着这个例子 {3,1,4,5,2} 多看一会,想想怎么才能用最简便的算法求逆序,换言之,什么时候可以算一个逆序。逆序是针对正序说的,而正序就是已经从小到大排好序。所以有一种思路是记录初始时每个元素的下标,然后对数组进行排序,算初始和最终下标之差便可得到某个数的逆序。例如 3 之后会和 1 2 构成两组逆序,而排序完成后 1 2 必然会在 3 之前,这就意味着 3 后移了两位,所以 3 的逆序是 2;而对于 1,1 排序后位置前移了,说明它和它后面的元素不构成逆序,便不计入最终结果。以上思路的实现,需要一个结构体数组,data域存储数据,p域存储初始位置,然后以data为关键字对结构体数组进行排序,排完后遍历一遍,用p减去循环变量i即可得到第i个元素的逆序。如此实现,时间复杂度会包含给结构体数组赋值的时间O(n),归并排序的时间O(nlog2n),遍历比较的时间O(n),总时间复杂度为O(nlog2n);空间复杂度包含一个结构体数组O(n),归并辅助数组O(n),总空间复杂度为O(n)。

还有更简单的方法吗?上面已经考虑到了元素有序和无序的比较,但那是在排序完之后统计的,很自然会想到如果能在排序过程中就统计出逆序,则可以节省出O(n)构造结构体数组和O(n)遍历比较的时间开销,变为纯粹的归并O(nlog2n),并且还可以节省出O(n)结构体数组的空间开销。按照这个思路,开始下面的讨论。对于 {3,1,4,5,2} 这个数组来说,能否类似归并,用递归的思路统计逆序?那么模仿归并的划分过程,最深层递归是({3}{1})({4}{5}({2}),这里用括号表示两部分同属于一层递归。按照正常思路,计算3的逆序,则是把3和1 4 5 2依次比较,此处3 1 同属一层递归,4 5 同属一层递归,2 单独属于一层递归。那么 3 和 1 4 5 2 四个数的比较便分散到了三层递归中,例如在当前这层递归中 3 1 同属一层递归,所以3 只能和 1 比较,回到上一层递归后,{3,1}和{4,5}同属一层递归,此时对两个部分进行排序,会变为{1,3}和{4,5},会发现{1,3}中的所有元素均小于{4,5},所以这两组之间没有逆序;再向上一层递归,{1,3,4,5}和{2}同属于一层递归,而3 4 5均大于2,所以有三个逆序。

将以上的分析转化为算法思想,便是:

使用归并排序,在同一层递归中,上一层的子列会被划分为A和B两个子列,对他们进行排序后,A和B内部各自有序,此时遍历A中元素和B中元素,若A[i]大于B[j],因为A中元素有序,所以A[i]及它之后的所有元素都比B[j]大,此时会有A的最大下标(其实是mid)-i+1个逆序。其实也就是在归并的“并”这一过程中增加了一个比较环节而已,并不增加时间复杂度。

int cnt=0;//全局计数变量统计逆序
void Merge(int A[], int low, int mid, int high){
    int i, j, k;
    for(k=low; k<=high; k++){//构造辅助数组B
        B[k] = A[k];
    }
    for(i=low, j=mid+1, k=i; i<=mid&&j<=high; k++){//两个有序合成一个有序
        if(B[i]<=B[j]){
            A[k] = B[i++];
        }
        else{//如果B[i]>B[j]
            A[k] = B[j++];
            cnt+=mid-i+1;//则B[i]后面每一个都大于B[j],直接计算逆序加入cnt
        }
    }
    while(i<=mid) A[k++] = B[i++];
    while(j<=high) A[k++] = B[j++];
}

void MergeSort(int A[], int low, int high){//归并主体
    if(low<high){
        int mid = (low+high)/2;
        MergeSort(A, low, mid);
        MergeSort(A, mid+1, high);
        Merge(A, low, mid, high);
    }
}

int order(int A[], int n){
    int *B=(int *)malloc(sizeof(int)*n);//为辅助数组B分配空间
    MergeSort(A, 0, n-1);//归并同时计算逆序
    return cnt;//返回
}

17.如果一个序列是一个先单调递增后单调递减的序列,那么它称为双调序列。设计一个尽可能高效的算法,找到由N个数组成的一个双调序列中最大的关键值(真题2021)

思路:看到单调序列第一个想到的就是二分查找,但是这是个双调序列,那如果增加判定条件能否使用二分查找?尝试分析,例如还是使用中间位置元素将序列进行分割,比较A[mid]和A[mid-1],若A[mid]>A[mid-1],则说明当前mid位置的元素在递增序列中,再比较A[mid]和A[mid+1],若A[mid]>A[mid+1],则说明当前mid位置的元素在递减序列中。而同时满足以上两个条件,即又在递增序列中又在递减序列中,那么必然处于两部分的交界处,也即是最大值所在位置。那么如果不同时满足以上两个条件,如A[mid-1]<A[mid]<A[mid+1],此时mid位置元素是在递增序列的中间,那么可以以mid+1~high为界,开始下一次的划分;同理,若A[mid-1]>A[mid]>A[mid+1],此时mid位置元素是在递减序列的中间,那么可以以low~mid-1为界,开始下一次的划分。

另外还需讨论一下边界条件,左边,mid-1<low,即mid=low+1,也就说明经过若干次划分mid移到了最左边,此时整个序列是单调递减的,所以直接返回A[0]。另一边,mid+1>n-1,即mid=n-1,说明经过多次划分mid移到了最右边,此时整个序列是单调递增的,所以直接返回A[n-1]。综上所述,当mid-1<0或mid+1>n-1时,直接返回A[mid]。

最后,该算法时间复杂度和空间复杂度都为递归深度O(log2n)。

int bisearch(int A[], int low, int high){
    int mid=(low+high)/2;//取中间位置
    if(mid-1<low || mid+1>high) return A[mid];//退出递归的边界条件
    if(A[mid-1]<A[mid] && A[mid]>A[mid+1]) return A[mid];//mid在两单调序列的中间
    //mid位于递增序列中,则向右查找
    else if(A[mid-1]>A[mid] && A[mid]>A[mid+1]) bisearch(A, low, mid-1);
    //mid位于递减序列中,则向左查找
    else if(A[mid-1]<A[mid] && A[mid]<A[mid+1]) bisearch(A, mid+1, high);
}

18.设有一个正整数序列组成的有序单链表(递增,允许相等),设计一个尽可能高效的算法实现以下功能:a.确定在序列中比正整数x大的数有几个(相同的数只计算一次,如{3 5 6 6 8 10 11 13 13 16 17 20 20}中比10大的数有5个);b.将单链表中比正整数x小的数按递减次序排列;c.将比正整数x大的偶数从单链表中删除(真题2021)

思路:以题目所给序列为例分析:{3 5 6 6 8 10 11 13 13 16 17 20 20},数比10大的数很简单,着重讨论后两问。第二问需要将比x小的数按递减次序排列,考虑在10之前或者小于10的最后一个结点之后插入,例如此例中3566,依次插入到8后,得86653,符合要求。然后第三问,继续向后遍历,每个数对2取余判断是否为偶数,为偶数则删除。

void func(LinkList L, int x, int &cnt){//cnt 用来返回大于 x 的元素数量
    cnt = 0;//初始化记计数变量
    LNode *p = L->next;//p指向第一个结点(有头结点)
    while(p->next!=NULL&&p->next->data<x){// p 指向小于 x 的最后一个元素
        p = p->next;
    }
    LNode *r = p->next;//标记大于等于 x 的第一个元素的位置
    LNode *q = L->next;//q指向表头
    while(q!=p){//把 p 之前的每一个元素,按照头插法插入到 p 后
        LNode *s = q->next;
        q->next = p->next;
        p->next = q;
        q = s;
    }
    L->next = p;//L指向 p,此时以在原链表中将小于x的元素逆置完成
    p = r;//p 指向第一个大于等于 x 的元素位置
    while(p->next!=NULL && p->next->data<=x)//从 r 开始找到最后一个不大于 x 的元素
        p = p->next;
    r = p;//r 标记最后一个不大于 x 的元素的位置,也即第一个大于x元素的前驱
    p = p->next;//p指向p的后继
    while(p!=NULL){//对链表遍历
        if(p->data!=p->next->data) cnt++;//若当前元素和后一个元素不相等,则cnt++
        if(p->data%2==0){//如果是偶数则删除
            r->next = p->next;
            free(p);
            p = r->next;
        }
        else{//如果不是,r和p均后移一位
            r = p;
            p = p->next;
        }
    }
}

19.设计一个算法,将一个用循环链表表示的稀疏多项式分解成两个多项式,使这两个多项式中各自仅含奇次项或偶次项,并要求利用原链表中的结点空间构成这两个链表。(真题2022)

提示:其中稀疏多项式采用的循环链表存储结构 LinkedPoly 定义为:

思路:太简单了就不说了。

typedef struct PolyNode{
    float coef; //单项式的系数
    int exp; //单项式的指数
    struct PolyNode *next;
}PolyNode;
typedef PolyNode* LinkedPoly;

void Divide(LinkedPoly L){
    PolyNode *Leven = (PolyNode *)malloc(sizeof(PolyNode));
    PolyNode *Lodd = (PolyNode *)malloc(sizeof(PolyNode));
    Leven->next = NULL; Lodd->next = NULL;
    PolyNode *p = L->next;
    while(p!=L){
        PolyNode *s = p->next;
        if(p->exp%2==0){
            p->next = Leven->next;
            Leven->next = p;
        }
        else{
            p->next = Lodd->next;
            Lodd->next = p;
        }
        p = s;
    }
}

20.从根到叶子的最大距离称为树的半径。给定一个无向连通图,写一个算法找出半径最小的生成树(真题2022)

思路:生成一个到叶子距离最大长度最小的生成树,换言之,只统计最下层(第h层,h为高)的叶子,小于h层的叶子到根结点距离必然小于h层叶子到根结点的距离。也就是说要生成一个高度最小的生成树,即广度优先生成树。Based on the circumstance that mutiple structures are required, the solution to this question could be a complicated process, which means that you should manually define the necessary data structures and give the implementation process of some primary functions.(注:以下代码没有编译运行测试)

#include <stdio.h>

#define MAX_VEX_SIZE 10

//图结构体
typedef struct Graph{
    char vex[MAX_VEX_SIZE];
    int arc[MAX_VEX_SIZE][MAX_VEX_SIZE]={0};
    int vexnum, arcnum;
}Graph;

//树结构体
typedef struct TNode{
    char data;
    TNode *child[MAX_VEX_SIZE];
    int childnum;
}TNode, *Tree;

//队列结构体
typedef struct Queue{
    TNode *data[MAX_VEX_SIZE];//队列中的数据域是树的结点类型
    int rear, front;
}Queue;

//图初始化
void InitGraph(Graph &G){
    Q.vexnum = 0;
    Q.arcnum = 0;
}

//给定字符型顶点e,返回e在vex数组中下标
int GetIndex(Graph G, char e){
    for(int i=0, i<G.vexnum; i++){
        if(G.vex[i]==e) return i;
    }
    return -1;
}

//给定下标i,返回对应的顶点
char GetVex(Graph G, int i){
    return G.vex[i];
}

//插入顶点e
void InsertVex(Graph &G, char e){
    G.vex[vexnum++] = e;
}

//在顶点src和tar之间创建边
void InsertArc(Graph &G, char src, char tar){
    int s = GetIndex(G, src);
    int t = GetIndex(G, tar);
    if(s!=-1&&t!=-1){
        G.arc[s][t] = 1;
        G.arc[t][s] = 1;
    }
}

//找到vex数组中下标为src的顶点的第一个邻接点
int FirstAdjVex(Graph G, int src){
    for(int j=0; j<G.vexnum; j++){
        if(G.arc[src][j]) return j;
    }
    retunr -1;
}

//找到vex数组中下标为src的顶点在basis之后的第一个邻接点
int NextAdjVex(Graph G, int src, int basis){
    for(int j=basis+1; j<G.vexnum; j++){
        if(G.arc[src][j]) return j;
    }
    return -1;
}

//初始化队列
void InitQueue(Queue &Q){
    Q.rear = 0;
    Q.front = 0;
}

//入队操作(默认MAX_VEX_SIZE大于队列使用长度)
void EnQueue(Queue &Q, TNode *node){
    Q.data[Q.rear] = node;
    Q.rear = (Q.rear+1)%MAX_VEX_SIZE;
}

//出队操作
TNode* DeQueue(Queue &Q){
    TNode *ret = Q.data[Q.front];
    Q.front = (Q.front+1)%MAX_VEX_SIZE;
}

//判空(默认MAX_VEX_SIZE大于队列使用长度)
bool Empty(Queue Q){
    if(Q.rear==Q.front) return true;
    return false;
}

bool visited[MAX_VEX_SIZE];
Tree BFST(Graph G, char src, int &h){//从顶点src开始生成BFS树,h返回树高
    int cnt = 0;//用于统计一层的结点个数
    int srci = GetIndex(G, src);//找到BFS生成树起始点src在vex数组中的位置
    for(int i=0; i<G.vexnum; i++){//修改所有顶点的visited值为未访问
        visited[i] = false;
    }
    Queue Q;//创建队列Q辅助BFS过程
    InitQueue(Q);//队列初始化置空
    visited[srci] = true;//顶点src标记为已访问
    //生成根结点
    Tree root = (Tree)malloc(sizeof(TNode));//创建根结点
    root->data = src;//根结点值为src
    root->childnum = 0;//根结点目前无孩子
    EnQueue(Q, root);
    cnt = 1;//第一层有一个结点
    //BFS遍历
    while(!Empty(Q)){
        TNode *father = DeQueue(Q)//出队一个结点,该结点为当前生成子树的根结点
        cnt--;//当前层每遍历一个结点,cnt-1
        if(cnt==0) h++;//遍历完一层后,h++
        int src = GetIndex(father->data);//根节点中所存顶点在图中的下标
        for(int i=FirstAdjVex(G, src); i>=0; i=NextAdjVex(G, src, i)){
            cnt++;//当前层结点个数+1
            char v = GetVex(G, i);//src的邻接点v
            TNode *node = (Tree)malloc(sizeof(TNode));//创建结点node
            node->data = v;//node的data域为v
            node->childnum = 0;//node的孩子个数为0
            EnQueue(Q, node);//node入队
            father->child[father->childnum++] = node;//node连接到当前生成子树的下面
        }
    }
    return root;
}

Tree MHBFST(Graph G, int &h){
    h = MAX_VEX_SIZE;
    Tree res;
    for(int i=0; i<G.vexnum; i++){//循环遍历图中每个顶点
        char src = GetVex(G, i);//记录当前顶点的数据
        int bfsh;//用于暂存BFS生成树高
        Tree ret = BFST(G, src, bfsh);//记录生成树返回的根结点
        if(bfsh<=h){//如果生成树高比h小
            h = bfsh;//更新h
            res = ret;//最终结果记为上一步返回的生成树的根
        }
    }
    return res; 
}

21.求给定二叉树两个结点的最近公共祖先(模拟1)

22.编写函数Find(BSNode *root, int key),其功能是在以结点root为根的BST中找比key大的最小值。若找不到返回NULL否则返回结点地址(模拟2)

23.设计一个算法,求给定BST中最小和最大的关键字(模拟3)

24.现有两个升序链表head1和head2,请完成算法,使这两个链表合并后仍然有序(模拟3)

25.判断二叉树是否为完全二叉树(模拟4)

26.输出二叉树从右到左第K个叶结点(模拟4)

27.将数组A[0...n-1]划分为左右两个部分,使得左边的所有元素为奇数,右边的所有元素均为偶数。要求辅助空间O(1),时间O(n)(模拟4)

28.二叉树,使用三叉链表(*parent)表示,并增加flag域(可以取0、1、2三个值,0表示访问根结点,1表示访问左孩子,2表示访问右孩子),在该存储结构上,设计一个算法实现非递归不用栈的后序遍历算法(模拟5)

29.已知一个升序排列的数组和一个数字,设计一算法Findsum,在数组中查找两个数,使得他们的和恰好等于已知数字,例如数组1、2、4、6、7、11和11。4+7=11,因此输出4和7。若存在多对结果,输出任意一对(模拟5)

  • 8
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值