算法之美一书附录中笔试面试题目参考答案

探秘算法世界,求索数据结构之道;汇集经典问题,畅享编程技法之趣;点拨求职热点,敲开业界名企之门。《算法之美——隐匿在数据结构背后的原理》全文目录“45个算法”目录“22个经典问题目录”,请见如下链接:

算法之美隆重上市欢迎关注(更有三重好礼)

http://blog.csdn.net/baimafujinji/article/details/50484348


*本书附录中的笔试面试题目主要从我之前的系列博文《常见C++笔试题目整理(含答案)》中摘录挑选。更多题目请参见


常见C++笔试题目整理(含答案)1

常见C++笔试题目整理(含答案)2

常见C++笔试题目整理(含答案)3

常见C++笔试题目整理(含答案)4

常见C++笔试题目整理(含答案)5

常见C++笔试题目整理(含答案)6

常见C++笔试题目整理(含答案)7

常见C++笔试题目整理(含答案)8

常见C++笔试题目整理(含答案)9

常见C++笔试题目整理(含答案)10


1. 答:

BOOL : if ( !a ) or if(a)
int : if ( a == 0)
float : const EXPRESSION EXP = 0.000001
if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL) 


2. 答:

方法一

16位的系统下,


int i = 65536;
cout << i; // 输出0;
int i = 65535;
cout << i; // 输出-1;

32位的系统下,
int i = 65536;
cout << i; // 输出65536;
int i = 65535;
cout << i; // 输出65535;

方法二
int a = ~0;
if( a>65536 )
{
    cout<<"32 bit"<<endl;
}
else
{
    cout<<"16 bit"<<endl;
}


3. 答:

方法一

101个先取出2堆,
33,33
第一次称,如果不相等,说明有一堆重或轻
那么把重的那堆拿下来,再放另外35个中的33
如果相等,说明假的重,如果不相等,新放上去的还是重的话,说明假的轻(不可能新放上去的轻)
第一次称,如果相等的话,这66个肯定都是真的,从这66个中取出35个来,与剩下的没称过的35个比
下面就不用说了

方法二

第3题也可以拿A(50),B(50)比一下,一样的话拿剩下的一个和真的比一下。
如果不一样,就拿其中的一堆。比如A(50)再分成两堆25比一下,一样的话就在
B(50)中,不一样就在A(50)中,结合第一次的结果就知道了。


4. 答:

(1) const char * p

一个指向char类型的const对象指针,p不是常量,我们可以修改p的值,使其指向不同的char,但是不能改变它指向非char对象,如:


const char *p;
char c1='a';
char c2='b';
p=&c1;//ok
p=&c2;//ok
*p=c1;//error

(2)char const * p

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;

(3) char * const p
如果const位于星号的右侧,则const就是修饰指针本身,即指针本身是常量。此时*p可以修改,而p不能修改。


5. 答:

引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。 


6. 答:

(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。 


7. 答:

如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;
例1


int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确

例2


string foo();
void bar(string & s);

那么下面的表达式将是非法的:


bar(foo());
bar("hello world");

原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。

引用型参数应该在能被定义为const的情况下,尽量定义为const 。 


8. 答:

格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }
好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!
注意事项:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。 这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。 这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)流操作符重载返回值申明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。 因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
例3


#include <iostream.h>
int &put(int n);
int vals[10];
int error=-1;

void main()
{
	put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
	put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;
	cout<<vals[0];
	cout<<vals[9];
}

int &put(int n)
{
	if (n>=0 && n <=9 ) return vals[n];
	else { cout<<"subscript error"; return error; }
}



(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一 个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。


9. 答:流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。


10. 答:正确。这个 sizeof 是编译时运算符,编译时就确定了。可以看成和机器有关的常量。


11. 答:数组越界


12. 答:数组越界


13. 答:

==数组越界
==strcpy拷贝的结束标志是查找字符串中的/0 因此如果字符串中没有遇到/0的话 会一直复制,直到遇到/0,上面的123都因此产生越界的情况

建议使用 strncpy 和 memcpy


14. 答:

都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new 会自动调用对象的构造函数。delete 会调用对象的destructor,而free不会调用对象的destructor。


15. 答:

1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。


16. 答:

p = malloc(1024);     应该写成: p = (char *) malloc(1024);
没有释放p的空间,造成内存泄漏。


17. 答:


#include <iostream>
using namespace std;

void main()
{
	char a[50];memset(a,0,sizeof(a));
	int i=0,j;
	char t;
	cin.getline(a,50,'/n');

	for(i=0,j=strlen(a)-1;i<strlen(a)/2;i++,j--)
	{
		t=a[i];
		a[i]=a[j];
		a[j]=t;
	}
	cout<<a<<endl;
}


18. 答:


char *strcpy(char *strDest, const char *strSrc)
{
	if ( strDest == NULL || strSrc == NULL)
		return NULL ;
	if ( strDest == strSrc)
		return strDest ;
	char *tempptr = strDest ;
	while( (*strDest++ = *strSrc++) != ‘/0’);
	return tempptr ;
}


19. 答:


String::String(const char *str)
{
	if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
	{
		m_data = new char[1] ;
		m_data[0] = '/0' ;
	}
	else
	{
		m_data = new char[strlen(str) + 1];
		strcpy(m_data,str);
	}
}

String::String(const String &another)
{
	m_data = new char[strlen(another.m_data) + 1];
	strcpy(m_data,other.m_data);
}

String& String::operator =(const String &rhs)
{
	if ( this == &rhs)
		return *this ;
	delete []m_data; //删除原来的数据,新开一块内存
	m_data = new char[strlen(rhs.m_data) + 1];
	strcpy(m_data,rhs.m_data);
	return *this ;
}

String::~String()
{
	delete []m_data ;
}


20. 答:显然这个问题就是写个KMP算法,时间复杂度是O(n+m)。


21. 答:

(1)


Node * ReverseList(Node *head) //链表逆序
{
	if ( head == NULL || head->next == NULL )
		return head;

	Node *p1 = head ;
	Node *p2 = p1->next ;
	Node *p3 = p2->next ;
	p1->next = NULL ;

	while ( p3 != NULL )
	{
		p2->next = p1 ;
		p1 = p2 ;
		p2 = p3 ;
		p3 = p3->next ;
	}

	p2->next = p1 ;
	head = p2 ;
	return head ;
}



(2)


Node * Merge(Node *head1 , Node *head2)
{
	if ( head1 == NULL)
		return head2 ;
	if ( head2 == NULL)
		return head1 ;

	Node *head = NULL ;
	Node *p1 = NULL;
	Node *p2 = NULL;

	if ( head1->data < head2->data )
	{
		head = head1 ;
		p1 = head1->next;
		p2 = head2 ;
	}else{
		head = head2 ;
		p2 = head2->next ;
		p1 = head1 ;
    	}

	Node *pcurrent = head ;

	while ( p1 != NULL && p2 != NULL)
	{
		if ( p1->data <= p2->data )
		{
			pcurrent->next = p1 ;
			pcurrent = p1 ;
			p1 = p1->next ;
		}else{
			pcurrent->next = p2 ;
			pcurrent = p2 ;
			p2 = p2->next ;
		}
	}

	if ( p1 != NULL )
		pcurrent->next = p1 ;
	if ( p2 != NULL )
		pcurrent->next = p2 ;

	return head ;
}


(3)


Node * MergeRecursive(Node *head1 , Node *head2)
{
	if ( head1 == NULL )
		return head2 ;
	if ( head2 == NULL)
		return head1 ;

	Node *head = NULL ;
	if ( head1->data < head2->data )
	{
		head = head1 ;
		head->next = MergeRecursive(head1->next,head2);
	}else{
		head = head2 ;
		head->next = MergeRecursive(head1,head2->next);
	}

	return head ;
}



22. 答:


一种O(n)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然)。


struct node { char val; node* next;}

bool check(const node* head)
{
	if(head==NULL) return false;
	node *low=head, *fast=head->next;

	while(fast!=NULL && fast->next!=NULL)
	{
		low=low->next;
		fast=fast->next->next;
		if(low==fast) return true;
	}
	return false;
}



23. 答:大体思路是建立一个辅助栈,把输入的第一个序列中的数字依次压入该辅助栈,并按照第二个序列的顺序依次从该栈中弹出数字。分析入栈、出栈的过程,可以找到判断一个序列是不是栈的弹出序列的规律:如果下一个弹出的数字刚好是栈顶数字,那么直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了,却仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。


24. 答:会引起无限递归。


25. 答:


void fun( int a )
{
    printf( "%d", a%10 );
    a /= 10;

    if( a <=0 )return;
        fun( a );
}


26. 答:为了求得树的深度,可以考虑采用递归的方法,先求左右子树的深度,取二者较大者加1即是树的深度,递归返回的条件是若节点为空,返回0。

int FindTreeDeep(BinTree BT){
    int deep=0;
    if(BT)
    {
        int lchilddeep=FindTreeDeep(BT->lchild);
        int rchilddeep=FindTreeDeep(BT->rchild);
        deep=lchilddeep>=rchilddeep?lchilddeep+1:rchilddeep+1;
    }
    return deep;
}

27. 答:你可能会想到用暴力搜索的方法:每次删除一个节点,然后判定图是否仍然连通,如果不再连通,那么删除的点就是割点。否则,就把图复原(把删除的点填回来),再重复删除其他节点和判定连通性的步骤。

这个方法是可行的,但显然不是我们想要的。一个比较著名的方法是由Robert Tarjan发明的割点检测算法。大致思路是:对图深度优先搜索,定义DFS(u)为u在搜索树(以下简称为树)中被遍历到的次序号。定义Low(u)为u或u的子树中能通过非父子边追溯到的最早的节点,即DFS序号最小的节点。根据定义,则有:Low(u)=Min { DFS(u) DFS(v) (u,v)为后向边(返祖边) 等价于 DFS(v)<DFS(u)且v不为u的父亲节点 Low(v) (u,v)为树枝边(父子边) }

一个顶点u是割点,当且仅当满足(1)或(2)

  • (1) u为树根,且u有多于一个子树。
  • (2) u不为树根,且满足存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得DFS(u)<=Low(v)。

更详细的原理请参考图论有关的材料。


28. 答:此处简要给出解答思路不具体给出代码。在后续遍历得到的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,所有元素都应该大于跟结点,因为这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,然后递归地确认序列的左、右两部分是不是都是二元查找树。


29. 答:


/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

struct SubTreeInfo{  
     int height;  
     bool isBalanced;  
     SubTreeInfo(int height, bool isBalanced): height(height), isBalanced(isBalanced) {};  
 }; 
 
class Solution {  
public:  
    bool isBalanced(TreeNode *root) {  
        SubTreeInfo r = balanced(root);  
        return r.isBalanced;  
    }  
      
    SubTreeInfo balanced(TreeNode* node){  
        if(node == NULL){  
            return SubTreeInfo(0, true);  
        }  
        else{  
            SubTreeInfo left = balanced(node->left);  
            SubTreeInfo right = balanced(node->right);  
            int h = (left.height > right.height ? left.height : right.height) + 1;  
            bool b = left.isBalanced && right.isBalanced;  
            b = b && ((left.height - right.height >= -1)  && (left.height - right.height <= 1));  
            return SubTreeInfo(h, b);  
        }  
    }  
};


30. 答:请参照《算法之美》一书P354、P356所给出之例子作答。


31. 答:


const int MINNUMBER = -32767 ;
int find_sec_max( int data[] , int count)
{
	int maxnumber = data[0] ;
	int sec_max = MINNUMBER ;
	for ( int i = 1 ; i < count ; i++)
	{
		if ( data > maxnumber )
		{
			sec_max = maxnumber ;
			maxnumber = data ;
		}
		else
		{
			if ( data > sec_max )
				sec_max = data ;
		}
	}
	return sec_max ;
}


32. 答:这道题目的解决办法有很多。例如,可以用quick sort对数组进行(从小到大)排序,然后找到第k个数,则比它的小的数都在其左侧,而比它大的数则都在其右侧。


33. 答:


#include<iostream>
#include<fstream>
using namespace std;

void Order(vector<int>& data) //bubble sort
{
	int count = data.size() ;
	int tag = false ; // 设置是否需要继续冒泡的标志位
	for ( int i = 0 ; i < count ; i++){
		for ( int j = 0 ; j < count - i - 1 ; j++){
			if ( data[j] > data[j+1])
			{
				tag = true ;
				int temp = data[j] ;
				data[j] = data[j+1] ;
				data[j+1] = temp ;
			}
		}
		if ( !tag ) break ;
	}
}

void main( void )
{
	vector<int>data;
	ifstream in("c://data.txt");
	if (!in)
	{
		cout<<"file error!";
		exit(1);
	}

	int temp;
	while (!in.eof())
	{
		in>>temp;
		data.push_back(temp);
	}

	in.close(); //关闭输入文件流
	Order(data);
	ofstream out("c://result.txt");
    
	if ( !out)
	{
		cout<<"file error!";
		exit(1);
	}

	for ( i = 0 ; i < data.size() ; i++)
		out<<data<<" ";

	out.close(); //关闭输出文件流
}


VIEWER DISCRETION IS ADVISED

上述答案如有缺漏敬请赐教。


你可能感兴趣的:(数据结构,C++,算法,IT求职应聘)