选择题
持续更到二十号叭
记录一些纠结比较久的选择题
有组记录的排序码为{46,79,56,38,40,84 },采用快速排序(以位于最左位置的对象为基准而)得到的第一次划分结果为:
(5分)
A.{38,46,79,56,40,84}
B.{38,79,56,46,40,84}
C.{38,46,56,79,40,84}
D.{40,38,46,56,79,84}
选D。刚开始,自己怎么排都不对,结果其实是,按照46<->40,79<->46,46<->38,56<->46的顺序来的,算是一种新的快排交换方式,即每次交换都需要用到基准数
对N个记录进行归并排序,空间复杂度为:(5分)
A.O(logN)
B.O(N)
C.O(NlogN)
D.O(N2)
归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n);还有,其时间复杂度为nlogn,其中的n的理解:若有n个数,从其n/2和n/2的状态合并为n,复杂度为n;其中的logn的理解:上述合并的过程其实是一颗二叉树,logn层;即每层合并至上一层的时间为n,共logn层,则为nlogn
排序过程中,对尚未确定最终位置的所有元素进行一遍处理称为一“趟”。下列序列中,不可能是快速排序第二趟结果的是:(5分)
A.5, 2, 16, 12, 28, 60, 32, 72
B.2, 16, 5, 28, 12, 60, 32, 72
C.2, 12, 16, 5, 28, 32, 72, 60
D.5, 2, 12, 28, 16, 32, 72, 60
快速排序基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。是一种分治思想的应用。
也就是说,快排一趟下来,找到一个基准数,左边的数字全比基准数小,右边的数字全比基准数大,那么,这个基准数就处在一个正确位置,即其位置为最终结果位置,可以用反证法证明一下。那么,第一趟找到的基准数如果能够将序列分为两部分,在第二趟,还能够分别从这两部分再得到两个基准数,共可找到三个基准数,但是如果第一趟找到的基准数在两端,那么下一趟,也只能再找出一个基准数了。选项D中,可发现,12和32为正确位置,但其并非位于两端,则序列中本应有三个正确位置的数字才对
动态规划算法的基本要素为( )。(2分)
A.最优子结构性质与贪心选择性质
B.深度优先与重叠子问题性质
C.最优子结构性质与重叠子问题性质
D.重叠子问题性质与贪心选择性质
补充:
动态规划(Dynamic programming)是一种算法设计技术,是用来解决一种多段决策过程最优的通用方法。多阶段决策过程模型共三个特征,它们分别是最优子结构、无后效性和重复子问题。其中无后效性比较难理解,无后效性有两层含义,第一层含义是,在推导后面阶段的状态的时候,我们只关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的。第二层含义是,某阶段状态一旦确定,就不受之后阶段的决策影响。
为5个使用频率不等的字符设计哈夫曼编码,不可能的方案是( )。(2分)
A.00,100,101,110,111
B.000,001,01,10,11
C.0000,0001,001,01,1
D.000,001,010,011,1
前缀定义:设α1α2…αn-1αn为长度为n的符号串,称其子串α1…αi,(0≤i≤n)为该符号串的前缀.其中,A满足前缀码,但不是哈夫曼编码,因为哈夫曼树是最优二叉树,最优二叉树只有度数为零和二的结点,但根据A中画出的树,并不符合
无向图G有16条边,度为4的顶点有3个,度为3的定点有4个,其余顶点的度均小于3,则G至少有( )个顶点。(2分)
A.13
B.12
C.10
D.11
无向图,一个点的度=出度=入度,结合握手定理,选D
给定有权无向图如下。关于其最小生成树,下列哪句是对的?
A.边(B, A)一定在树中,树的总权重为23
B.边(D, C)一定在树中,树的总权重为20
C.最小生成树不唯一,其总权重为23
D.最小生成树唯一,其总权重为20
kruskal:该算法是求连通网的最小生成树的一种方法。与prim算法不同,它的时间复杂度为O(eloge)(e为网中的边数),所以,适合于求边稀疏的网的最小生成树
prim:该算法的时间复杂度为O(n2)。与图中边数无关,该算法适合于稠密图。
两种方法等价,即结果总权值是一样的,但可以看有多少边权重相同,如果有,那么最小生成树可能是不唯一的,根据kruskal算法画树,A-H,A-G两边可任取其一,故不唯一。又根据kruskal算法可知,权值最小边一定在最小生成树中,则选C
//下列代码的功能是利用堆排序将N个元素按非递减顺序排序。(程序填空题)
#define leftchild(i) ( 2*(i)+1 )
void PercDown( ElementType A[], int i, int N )
{ int child;
ElementType Tmp;
for ( Tmp = A[i]; leftchild(i) < N; i = child ) {
child = leftchild(i);
if (--填空--)//child+1<N&&A[child]<A[child+1]
child ++;
if (--填空--) //Tmp<A[child]
A[i] = A[child];
else break;
}
--填空--;//A[i]=Tmp;
}
void Heapsort( ElementType A[ ], int N )
{ int i;
for ( i = N / 2; i>= 0; i -- ) /* BuildHeap */
PercDown( A, i, N );
for ( i = N-1; i >0; i -- ) {
Swap( &A[ 0 ], &A[ i ] );
--填空--; //PercDown(A,0,i)
}
}
/* 不理解其中for循环的执行顺序的,可以调试一下这个
#include <bits/stdc++.h>
using namespace std;
int main() {
int k = 5;
int num = 66;
int z;
for (z = 0; k < 7;k=7) {
cout << "zhuang" << endl;
z = 66;
}
cout << z << endl;
return 0;
}
*/
这题奇怪在,根节点的下标为零,那么一个结点的左孩子的下标为( 2*(i)+1 ),区别于根节点为一的情况:其左孩子结点为2i;
其次,堆的构建并非一个一个插入,而是用了一个percDown函数,需要递归,两种方式构建出的堆是不一样的;
最后,需要明白,求非递减序列,即递增序列,要用到大根堆,即每次将最大的元素取出,放在树的最后一个位置,最后的效果就是一个递增序列
回溯法搜索状态空间树是按照( )的顺序。(2分)
A.广度优先遍历
B.中序遍历
C.层次优先遍历
D.深度优先遍历
说多了都是泪,当初没好好学,选D
前情提要:
①对于m叉树,如果每个分支点的出度恰好等于m,则称该树为m叉正则树。m=2时,该根树称为二叉正则树。若其所有树叶层次相同,称为二叉完全正则树,总而言之,二叉完全正则树就是比完全二叉树多了一个限制,即分支点的出度相同
②TSP问题:一个售货员必须访问n个城市,恰好访问每个城市一次,并最终回到出发城市。售货员从城市i到城市j的旅行费用是一个整数,旅行所需的全部费用是他旅行经过的的各边费用之和,而售货员希望使整个旅行费用最低。
③区分:与回溯法相关的是分支限界法,两种方法都需要构建解空间树(一会再说);先明确,回溯法用带有剪枝函数的深度优先搜索,而分支界限法用广度优先搜索,所以,直观上看,回溯法可以搜索问题的所有解和任意解,而广度优先,只用于最优问题,比如我之间发布的关于迷宫寻路的问题,解决的是寻找最短路径的问题
④解空间树(状态空间树),顾名思义,树的每一条从根节点到叶节点的路径,都为一个解,这句话很重要,可以尝试用下面两个图片理解一下。它分为两种:子集树和排列树(参考下面给出的图片),可以通过复杂度对这两种树进行简单区分:子集树复杂度为2^n,n表示有n次决策,每次决策都有两种选择,比如01背包问题,能构建出一个子集树,n层;排列树复杂度为n!,n表示有n中选择,每次选择之后,剩余选择的数量减一,比如上述的TSP问题,如果选择了一个城市作为下次的落脚点,那么无论如何你不能再把这个城市走一遍
总结:回溯法对解空间树进行搜索,即进行递归操作,利用剪枝函数阻止无用的递归进行,减少时间浪费,其活结点(需要继续判断紧随该节点其后的决策是否可行的结点为活结点)需等其所有可行子节点都被遍历后才从栈中弹出,而分支界限法使用的广度优先,使其每个结点只有一次成为活结点的机会,即结点出队之后,就成为死结点了,无需再访问
某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用什么存储方式最节省运算时间?(2分)
A.单链表
B.仅有尾指针的单循环链表 √
C.仅有头指针的单循环链表
D.双链表
相关:简单来说,头结点是一个结点:包含数据域和指针域。而头指针是指向一个链表第一个结点的一个指针。这俩货在本质上就不是一样的。头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义。头指针就是链表的名字,头指针仅仅是个指针而已,当存在头结点时头指针指向头结点。因为头指针代表了该链表,所以头指针必须存在,但可指向空,表示无头结点空链表。
由于是单循环链表,尾指针可以更方便地在尾部插入结点,也可以通过两次next找到首元结点(头结点后的第一个结点)(有头结点的情况)
缺陷:无法删除最后一个结点,因为无法得知最后一个结点的前驱,因此可用双链表解决此问题
复习,时间不多了。先捋一遍
分治法
应用:棋盘覆盖、归并排序、快速排序
关于归并:
自然归并:在自然归并排序中,直接从输入序列中找出已经存在的有序段。例如,在输入数列[4,8,3,1,5,6,2]中可以认定4个有序段: [4,8], [3,7, [1,5,6]和[2]。认定的方法是,从左至右扫描序列元素,若位置i的元素比位置i+1的元素大,则位置;便是一个分割点。然后归并这些有序段,直到剩下一个有序段。
归并迭代算法:二路归并排序的一种迭代算法是这样的:首先将每两个相邻的大小为1的子序列归并,然后将每两个相邻的大小为2的子序列归并,如此反复,直到只剩下一个有序序列。轮流地将元素从a归并到b,从b归并到a,实际上消除了从b到a的复制过程。
关于快排:
对有序的输入序列实施快速排序,却表现出最坏情况下的时间性能。也就是说,一个快速排序算法,它对有序表排序比对无序表排序要慢,这是让人痛苦的问题。不过,我们可以通过三值取中选择支点pivot
三值取中快速排序(median-of-three quick sort)在三元素a[leftEnd], a[(leftEnd+rightEnd )/2]和a[rightEnd]中选择大小居中的中值元素作为支点元素。例如,若三元素分别为5,9,7,则取7为支点元素。为此,最简单的做法是将中值位置上的元素与元素a[leftEnd]交换,然后继续使用快速排序,如果a[rightEnd]是中值元素,则将a[leftEnd]和a[rightEnd]交换,然后再将a[leftEnd]选为支点元素,而其他代码不动。
问题记录:书中代码归并排序mergepass找不到标识符;快排时间复杂度的递推方程不理解;
回溯法
6-1 输出n-皇后的所有可行解 (10分)
在n×n的国际棋盘上放置n个皇后,使得每个皇后都不受到攻击,即任意二个皇后不能位于同行或同列或同一条斜线上。解题思路:将棋盘从左至右,从上到下编号为0,…,n-1,皇后编号为0,…,n-1。设解为(x0,…, xn-1) , 皇后i放在棋盘的第i行的第xi列上。 约束条件为:
xi≠xj(不在一列上)
xi-i≠xj-j(不在一条斜线上)
xi+i≠xj+j(不在一条斜线上) 2)和3)等价于:|i-j|≠|xi-xj|
采用回溯算法思想,解决n个皇后问题的代码如下所示,请补充函数Place()的代码,要求输出n-皇后的所有可行解,输出列号(0~n-1)
函数接口定义:
bool place(int k, int i, int *x){ }
其中 k、 i 和 *x 都是用户传入的参数。
裁判测试程序样例:
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <cstring>
using std::cin;
using std::cout;
using std::endl;
int n; //皇后个数
bool place(int k, int i, int *x);
void nQueens(int k, int n, int *x);
int main()
{
cin>>n;
int *x = new int [n + 1];
if(n == 2 || n==3)
cout << "0" <<endl;
nQueens(0, n, x);
return 0;
}
/* 请在这里填写答案 */
void nQueens(int k, int n, int *x)
{
for(int i = 0; i < n; i++)//每一行都有n中放置方法
{
if(place(k, i, x))//判断第k+1行第i+1列能否放置皇后
{
x[k] = i;
if(k == n - 1)//最后一行放置完成
{
for(i = 0; i < n; i++) cout << x[i] << " ";
cout << endl;
}
else
nQueens(k+1, n, x);//继续放置下一行
}
}
}
//答案
bool place(int k, int i, int* x){
if (k == 0)return true;
else {
for (int n = 0; n < k; n++) {
if (x[n]==i||x[n] - n == i-k || x[n] + n == i+k) {
return false;
}
}
}
return true;
}