经过这么多笔试、面试后,总结了一些题目,这些题目出现在各大公司的招聘中,主要列举的是我在面试笔试中碰到的一些题目,比较基础,应该对于应聘中人还是有点提示作用,其中链表题目和字符串题目真的很重要,一摸一样的题目出现的可能性很大,当别人知道这些题目的答案,而自己不知道时,肯定在第一步筛选时就落后一步了,所以就将这些题目贴出来了,有些我写了一些答案,而有些没有写,因为很多书上都有。
同时再推荐一个资料,那就是程序员面试一百题,这个很好,据我的考试以及面试来看,迅雷考试的两个大题目(100分),amazon的两个大题目,金山一个大题目;还有些忘了,反正就是考的很多上面的原题目。最好能够将上面的题目一个一个的吃透,并默写出代码来。
语言与编译器的一些题目
(1) 内联函数、普通函数和宏的区别?[华为面试题]
(1) [内联函数与普通函数的区别]
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
(2) [内联函数与宏的区别]
①内联函数要做参数类型检查,在类型不一致时要进行类型转换。宏是在代码处不加任何验证的简单替代。
②宏不是函数,只是在编译前[编译预处理]将程序中有关字符串替换成宏体。内联函数是函数,但在编译中不单独产生代码,而是在编译时将有关代码嵌入到调用处。
(3) [内联函数适用情况]
①函数只有简单的几行。②函数内没有循环语句,如for,while。③在项目中该函数被调用多次。
(4) [内联函数不适用情况]
①函数代码比较长,此时使用内联将导致内存代价较高。②一个函数内有循环,该函数不宜定义为内联函数。③一个函数是递归函数时,不宜定义为内联函数。
(2) extern "C"的含义?[百度笔试题]
C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为:void foo(int x,int y).。 该函数在C编译器将编译成_foo的名字存放在库中。而C++的编译器将编译成_foo_int_int之类的名字。这也是为什么在C++中,函数的重载只能是参数的个数或者是类型不同,而不能是返回值不同。
extern "C"是C++提供给C的连接交换指定符号,用于解决名字匹配的问题。
(3) 指针和引用的区别 [n多公司都问了]
①非空区别:在任何情况都不能使用指向空值的引用。而指针却可以给它赋空值。即不指向任何东西。
②合法性区别:在使用引用之前不需要测试它的合法性,而指针却应该总是被测试,防止它为空。
③可修改区别:指针可以被重新赋值以指向另一个不同的对象。但是引用在初始化时确定了后,再也不能改变。
④大小的区别: 引用的大小是它原来被引用变量的大小,如果是int类型就是4字节,如果是dobule型就是8字节,而指针的大小是固定的4字节,指的是指针自己的大小并不是指向内容的大小。
⑤应用区别:如果出现不指向任何对象的可能或者在不同时刻指向不同的对象,则要使用指针。如果总是指向一个对象并且之后不会改变指向,那就使用引用。
(4) [宏定义] 宏定义时括号的问题[华为、PMC]
①使用宏选出现a和b两个数中的最大值
#define MAX(a,b) ((a)>(b)?(a):(b))
(5) sizeof,strlen [笔试题目]
[sizeof和strlen的使用情况]
第一个例子
char* ss = "0123456789";
sizeof(ss) 结果4 . ss是指向字符串常量的字符指针,对于一个指针来说,它在内存占用的空间一定是4个字节
sizeof(*ss) 结果1. *ss是第一个字符, sizeof对某种类型使用时,返回的是该类型在内存中占用的空间。
strlen(ss) = 10. Strlen的参数为字符数组,或者是字符指针,返回字符数组中的字符个数,不包括'\0'。
Strlen(*ss) 是错误的,因为*ss是一个字符,而不是一个字符串指针。
char ss[] = "0123456789";
sizeof(ss) 结果 11. ss是数组,计算到\0位置,因此是10+1
sizeof(*ss) 结果 1 . *ss是第一个字符
strlen(ss) = 10. Strlen的参数为字符数组,或者是字符指针,返回字符数组中的字符个数,不包括'\0'。
Strlen(*ss) 是错误的,因为*ss是一个字符,而不是一个字符串指针。
char ss[100] = "0123456789";
sizeof(ss) 结果是100 . ss表示在内存中的大小 100×1.s 虽然ss中只存放了11个元素,但是它的静态大小(即一开始定义的,系统分配给它的)为100.
strlen(ss) 结果是10. Strlen是一个函数,它不管你的数组是多大的,它返回的是里面存放的元素个数,所以它从头开始找,直到'\0'。
int ss[100] = "0123456789";
sizeof(ss) 结果 400 . ss表示再内存中的大小 100×4
strlen(ss) 错误. strlen的参数只能是char* 且必须是以''\0''结尾的
第二个例子:
class X {
int i;
int j;
char k;
};
X x;
class Y{
};
cout<<sizeof(X)<<endl; 结果 12。 由于X中参数字节最长的是int型,它是4字节的,未超过处理器的位数,所以它必须按4字节对齐,也就是说sizeof(X)必须能够整除4.所以最后一个成员k虽然只占了一字节,但是它后面有3字节是空着的。如果超过的处理器的位数,则按处理器的位数进行对齐。
cout<<sizeof(x)<<endl; 同上
sizeof(Y) = 1;
第三个例子:
char szPath[MAX_PATH]
如果在函数内这样定义,那么sizeof(szPath)将会是MAX_PATH,但是将szPath作为虚参声明时(void fun(char szPath[MAX_PATH])),sizeof(szPath)却会是4(指针大小)
[sizeof和strlen的区别]
(1) sizeof是运算符,strlen是函数。
(2) sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''\0''结尾的。sizeof还可以用函数做参数,比如:short f(); printf("%d\n", sizeof(f()));输出的结果sizeof(short)即2
(3) 数组做sizeof的参数不退化,传递给strlen就退化为指针了。
(4) 大部分编译程序 在编译的时候就把sizeof计算过了, 是用于计算类型或是变量的长度,这就是sizeof(x)可以用来定义数组维数的原因。strlen的结果要在运行的时候才能计算出来,是用来计算字符串的长度,不是类型占内存的大小。
(5) 当使用于一个结构体类型或变量时,sizeof 返回实际的大小,当适用一静态地空间数组,sizeof 归还全部数组的尺寸。sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸
(6) 数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,相当于一个指针,所以strlen的参数可以是字符串指针,也可以是字符数组首地址,但一定需要'\0'作为结尾。
(7)sizeof计算结构体变量,类的大小时,必须讨论数据对齐问题。
(6) 虚函数、纯虚函数,多态 [只要应聘C++岗位的必问]
(7) 拷贝构造函数与赋值构造函数
⑴ 如何限制一个类的对象只在栈上以及只在堆上生成[网易]
1 在C++中如何限制一个类对象只在堆上分配?
仿照设计模式中的单实例模式或者工厂模式来解决,这里采用单实例模式方式来说明。
将类的构造函数属性置为private,同时提供static成员函数getInstance,在函数中new一个新对象,然后返回对象指针或者引用。这样实现的类可以保证只可以在堆上分配对象。
2 在C++中如何限制一个类对象只在栈上分配?
重载类的new操作符,使重载后的new操作符的功能为空。这样就使外层程序无法在堆上分配对象,只可以在栈上分配。 [相当于废除new这个操作符,虽然是改变了运算符重载的一些需要遵守的规则,但是这还是可以的]
⑵ 实现String类的四种构造函数 [金山]
(8) 不使用第三个变量,交换两变量的值
第一种使用加减法:(需要考虑a+b溢出的问题)
a = a+b;
b = a-b;
a = a-b;
第二种使用乘除法:(需要考虑a*b溢出的问题,同时需要考虑两个b是否为0,除数不能为0)
a = a*b;
b = a/b;
a = a/b;
第三种使用异或:
a = a^b;
b = a^b;
a = a^b;
(9) new、delete与malloc、free的区别
① 不同点:new和delete对应,是C++中的运算符,使用new时会调用构造函数,使用delete时会调用对象的析构函数;malloc和free对应,是C++/C语言的标准库函数,它们在使用只是纯粹的为对象分配/释放空间。
② 相同点:他们都是用于申请动态内存和释放内存。
③ 为什么需要new\delete: 因为malloc和free无法满足动态对象的要求。因为对象在创建的时要调用构造函数,在消亡时调用析构函数,而malloc/free是库函数,不能将这些工作强加于malloc/free身上,所以C++需要一个能够动态内存分配和初始化工作的运算符new以及一个能够清理和释放内存的运算符delete。
(10) static 关键字的作用
static的五种含义:
① 修饰全局变量时,表明一个全局变量只对定义在同一个文件中的函数可见,即不能够使用extern关键字来扩展它的作用域,其存储类型是static型,即存放在静态数据区。
② 修饰局部变量时,表明该变量的值不会因为函数的终止而销毁,作用域是所定义的函数内。函数中的赋值语句只会执行一次.
③ 修饰全局函数时,表明该函数只能被定义在同一文件中的函数调用。
④ 修饰类的数据成员时,表明该类的所有对象将共享这个静态数据成员。
⑤ 修饰类的成员函数时,表明该函数不能够访问非静态数据成员,只能够访问它自己的参数、类的静态数据成员和全局变量。
(11) const关键字的用法
const的用法有以下几种:
① 修饰全局变量时,表明它是一个常量,作用域在有static修饰的时候是本文件的作用域,如果没有static修饰的时候作用域是全局的;(与#define宏定义的区别: ⑴ const定义的常量需要进行静态类型安全检查,而#define定义的常量只是在编译预处理时期,直接进行文本替换,没有类型检查。⑵ 有些编译器可以对const定义的常量进行调试,而#define不会进行调试。⑶ const定义常量不会出现边际效应,而#define的边际效益经常发生);
② 修饰局部变量时,同①;只是作用域变成了函数局部变量了;
例子:
const int Asize =100;
const int data[] = {1,2,3,4,5};
const struct cir c1 = {1,2,3};
// 分别表示整型变量Asize、数组data以及结构体c1不能够再改变
③ 修饰指针
⑴ 将指针所指向的对象定义成常量但指针不是常量;[const没有越过*号]
const <类型> *指针名 = 初始值;
<类型> const *指针名 = 初始值;
[例子]
const char *pStr = "abcd"; 或者char const* pStr = "abcd"
pStr[2] = 'c'; //出错;
pStr = "cdef"; // OK
⑵ 将指针定义为常量而指针指向的对象不是常量; [const越过*号]
<类型> *const 指针名 = 初始值;
[例子]
Char * const pStr = "abcd";
pStr = "defa"; //出错
pStr[2] = 'd'; // OK
⑶ 将指针以及指针指向的对象都定义为常量; [*号两边都有const]
Const <类型> * const 指针名 = 初始值;
Const char * const pStr = "abcd";
pStr = "dewfe"; // 出错
pStr[2] = 'd'; // 出错
④ const修饰函数的形参
表明在该函数中不能够改变该参数的值;具体要试形参的类型类看,如果形参是基本类型的变量,那同const修饰变量的用法,如果形参是指针,那同const修饰指针的用法;
⑤ const修饰函数的返回值
主要用于防止用户将一个函数的返回值作为表达式的左值来使用;比如const char*strCopy(char *dest, const char *src); 函数返回一个const的指针,那么我就不能够这样用了 strCopy(pStr, "abd") = "abcd";//出错
⑥ 类常量成员函数
将const用来类的成员函数形参表()与函数体{}之间的const.[与类的成员函数返回const类型值区别];主要表明该成员函数不能够修改调用该成员函数的对象的成员变量。[类常量成员函数不能够定义为static,即不能够定义为static int getX()cosnt{}; 因为static函数是属于类的,它能够修改的参数只有自己的形参、类的静态成员变量以及全局变量,而这三者都不属于一个类的对象的,所以一个类的常量成员函数没有必要定义为static,而定义为static时,编译器会报错]
比如:
class test {
public:
int getX()const {
return m_x;
}
private:
int m_x;
};
(12) 初始化成员列表和构造函数中赋值有什么区别?
① 有些情况下,必须使用初始化列表。特别是非static的const和引用数据成员初始化时必须使用初始化列表;
② 效率方面来说,对于内置成员或者复合类型,两者的效率差异不会很大,但是对于非内置数据类型,差异还是很明显的;在构造函数中赋值的非内置数据类型,必须调用一次缺省构造函数,然后调用一次赋值函数;而在初始化列表中,它只是初始化,并不需要调用赋值函数;
(13) 指针的一些问题
Int *p: 申明的是一个指向int型数的指针;
Int * p[10]: 申明的是一个指针数组,该数组有10个元素,每个元素都是一个指向int型数的指针;
Int (*p)[10]: 申明的是一个数组指针,该指针指向一个含有10个int型数的int数组;
Int *p(void): 申明了一个指针函数,即它是一个函数,函数的参数为空,函数名为p,该函数的返回值是一个指向int型数的指针;
Int (*p) (void) : 申明的是一个函数指针,指针名为p,它指向一个函数,该函数的返回值是int型,而参数是空;
Int(*p[10])(void): 申明的是一个函数指针数组,该数组有十个元素,每个元素是一个函数指针,指向的函数的参数为空,返回值为int;
数据库的知识
(14)事务的四个性质
⑴ Atomicity原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不能够出现那种停滞在中间某个环节,当事务在执行过程中发生错误时,会被回滚到事务开始前的状态,就像事务从来没有执行过一样。
⑵ Consistency 一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
⑶ Isolation 隔离性:两个事务的执行要保证互不干扰,一个事务不可能看到其他事务运行时中间某一时刻的数据。
⑷ Durability 持久性:在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
(15) 索引的优缺点?以及底层实现?
索引的优点:
⑴ 通过创建唯一性索引,可以保证数据库表中每行数据的唯一性;
⑵ 索引可以大大加快数据的检索速度;
⑶ 可以加速表和表之间的连接;
索引的缺点:
⑴ 创建索引和维护索引都要耗费时间,且这个时间会随数据量的增加而增加;
⑵ 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚族索引,那么需要的空间将会更大;
⑶ 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度;
索引的底层实现??????
(16) 数据库提供的两种语言
数据定义语言(DDL)和数据操纵语言(DML);
数据定义语言(DDL):是数据库中负责数据结构定义与数据库对象定义的语言,由CREATE\ALTER\DROP三个语法所组成。
数据操纵语言(DML):是用户通过它可以实现对数据的基本操作。例如,对表中数据的查询、插入、删除和修改。
(17) 数据库隔离的级别
隔离级别定义了事务与事务之间的隔离程度。实际上,隔离级别同并发性是相互矛盾的:隔离程度越高,数据库的并发性越差;隔离程度越低,数据库的并发性越好。
一般的隔离级别[SQL92标准定义]有:
未提交读 (read uncommitted)
提交读(read committed)
重复读(repeatable read)
序列化(serializable):使事务看起来,好像是一个接着一个的顺序执行。
通过一些现象,可以反映出隔离级别的效果。这些现象有:
更新丢失(lost update):当系统允许两个事务同时更新同一数据更新,发生更新丢失。
脏读(dirty read):当一个事务读取另一个事务尚未提交的修改时,产生脏读。
非重复读(nonrepeatable read):同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。[即指,多次读取同一数据,但是由于其他事务对其的修改,所以每次读取的结果都不一样]。
幻像读(phantom read):同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。
下面是隔离级别及其对应的可能出现或不可能出现的现象:
isolation level | Dirty Read | NonRepeatable Read | Phantom Read |
Read uncommitted | Possible | Possible | Possible |
Read committed | Not possible | Possible | Possible |
Repeatable read | Not possible | Not possible | Possible |
Serializable | Not possible | Not possible | Not possible |
(18) 数据库范式
(19)存储过程是什么?
存储过程(Stored Procedure) 是一组为了完成特定功能的SQL语句集,是利用SQL Server提供的Transact-SQL语言编写的程序。其功能是将常用或复杂的工作,预先用SQL语句写好并用一个指定名称存储起来,以后需要数据库提供与已定义好的存储过程的功能相同的服务时,只需要调用execute,即可自动完成命令。
网络的知识
(20) TCP/IP的五层结构
TCP/IP由五层构成: 应用层 ——> 传输层[TCP/UDP] ——>网络层[IP] ——>数据链路层 ——> 物理层; 而下面两层,数据链路层以及物理层又可以合并为一层,叫做网络接口层。
(21) TCP与UDP的区别
⑴ TCP是基于连接传输层协议,而UDP是面向非连接的传输层协议;
⑵ TCP提供可靠的通信连接,所以TCP保证数据的正确性,数据报的顺序,同时对系统资源的要求高;而UDP由于不需要连接,所以UDP可能丢包,并且数据包达到的顺序可能是随机的,但对系统的资源要求不高。
⑶ TCP是数据报模式的,其程序比较复杂,而UDP是流模式的,程序比较简单。
⑷ 应用场景不同,TCP主要用于数据准确性要求较高的应用:如FTP HTTP啊!而UDP常用于数据实时性较高、对数据准确性要求不那么严格的应用,如视频通话、IP通话;
(22) TCP的三次握手和四次断连
(23) TCP的四种定时器
⑴ 超时重传定时器: 在TCP连接以后,进行数据报的发送时,这个数据可能会丢掉,这个时候,对方由于不能够收到数据报而没有返回ack;所以需要为每一个数据包定义一个超时重传定时器,当定时器到时,还没有收到对方的ack时,就知道数据包丢失了,所以重发该数据报,并重新初始超时重传定时器;
⑵ 链路保活定时器:当客户端关闭、重启或崩溃或中间链路隔绝时,连接双方并不知道这些,而不断忘网络中发送数据,当数据丢失时,重复发送,而这会给网络带来严重的带宽问题;所以需要一个定时器来判断一条链路需要保持的时间,当链路保活定时器到时,如果还是不能够从对方得到回答,则终止这条链路;
⑶ 坚持定时器(窗口大小更改定时器):在使用滑动窗口算法时,当一端通知另外一端接收窗口为0时,另外一端只能够等待窗口大小不为0的ack数据报,而由于TCP不对ack数据发送ack确认包,当一个通知窗口大小的ack包丢失时,两端都等待对方的数据包,从而出现死锁的问题,解决办法就是在发送一个通知窗口大小的ack数据包时,定义一个定时器,这个定时叫做坚持定时器,当坚持定时器到时还没有收到对端新的数据包时,说明ack窗口通知数据报丢失,这个时候我们需要重新发送该ack窗口大小通知数据报,并重新启动一个坚持定时器。
⑷ 2MSL:在TCP的断连时,当一端A向另外一端B发送了FIN数据报后,接收到对端的ACK时,该TCP连接处于半连接状态,而当B端发送FIN过来时,A端接收到以后,A端向B端发送ACK,而此时A端处于TIME_2MSL时刻,因为TCP中不对ACK包进行ACK确认的,所以需要定义一个定时器来判断该ACK是否丢失,如果定时器时间到了,没有重新收到对端的FIN,则证明ACK没有丢失,对端已经成功终止连接。
(24) 使用TCP的应用层协议以及使用UDP的应用层协议
使用TCP的应用层协议:HTTP SMTP FTP TELNET
使用UDP的应用层协议: DNS SNMP SFTP TFTP
直接使用IP的协议有:
直接使用ICMP的协议有: PING
(25) 常见协议的端口号
FTP:21 HTTP:80 HTTPS:443 SMTP:25 DNS:53 TELNET:23 SSH:22 SFTP:115
POP3 110 TFTP:69
(26) socket 编程--描述流程
(27) I/O多路复用原理
(28) select、poll、epoll等的工作原理与区别
存储知识---RAID
(29) RAID系列的介绍知识
RAID0---将连续的数据分割到多个硬盘上;没有冗余数据,所以RAID0在所有RAID系列中的存储性能是最高的,同时它容错性是最差的,当一个磁盘上的数据损坏时,其他所有磁盘上的数据都不能够用;RAID0的读取速度是很高的,因为它没有校验数据验证;即RAID0适合于那种读取性能要求比容错要求更高的场景;
RAID1----镜像RAID,RAID1对于一份数据会完全存放两份,所以RAID1的存储效率是最差的,只有50%;但是它提供的安全性是最好的,因为它有两份数据,一个磁盘坏掉了,另外一个磁盘可以直接顶替上,完全可以不用停机;
RAID5----将数据和相对应的奇偶校验信息存储到RAID5的各个磁盘上,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上;当RAID5的一个磁盘发生损坏时,利用剩下的数据和相应的奇偶校验信息区恢复被损坏的数据。所以RAID5的可靠性很高,允许单个磁盘出错,同时RAID5的读取效率很高,但是写入效率一般,磁盘利用率为n-1,其对数据传输的并行性解决不好,且控制器的设计业比较困难。
RAID10----是RAID0与RAID1的组合体,它继承了RAID0的快速与RAID1的安全,RAID1在这里是一个冗余的备份阵列,而RAID0负责数据读写的阵列;RAID10具有RAID0极高的读写效率和RAID1较高的数据保护、恢复能力;但是它的存储利用率还是只有50%;
链表的题目
(30) 判断一个链表有没有环;当有环时,返回环的第一个节点?(扩展)判断两个单链表是否相交,返回相交的第一个节点?(扩展)遍历一个链表,该链表可能有环,返回链表的长度n?
(31) 返回链表的中间那个,如果是奇数个元素,则返回第(n+1)/2;如果是偶数个元素,则返回第n/2-1个元素?
(32) 给你一个链表的头指针,一个节点的指针,需要将这个指针删除掉,时间复杂度为O(1)?
(33) 返回链表倒数第k个元素(判断元素个数是否对于n)?
(34) 将一个链表进行逆序,采用迭代和递归两种方法?
需要考虑,链表是否是循环链表,是否链表内部有环否?
List *reverse (List *head) {
List *h = head;
List *temp, *new_head = NULL;
// 判空
if(h == NULL) return h;
do {
temp = h;
h = h->next;
temp->next = new_head;
new_head = temp;
// while 循环中的h != head就是判断链表是否是循环链表
}while(h != NULL && h != head)
}
(35) 约瑟夫环问题?[很重要,考了很多次,有数学方法,也有链表数组的方法]
(36) 两个链表head1和head2,各自有序,将两个链表进行合并并依然保持有序
两个链表并没有说都是递增还是递减;所以需要考虑
List* merge(List* head1, List*head2) {
// 假设链表有头结点
List *h1 = head1->next, *h2 = head2->next;
List *temp = head1, *h3=h1;
// flag1以及flag2为0时代表对应链表是升序的,为1时代表是降序的
int flag1 =0, flag2=0;
//判断空链表
if( h1 == NULL && h2 == NULL) {
return h1;
}else if(h1 == NULL) {
return h2;
}else if(h2 == NULL) {
return h1;
}
// 判断是升序还是降序,注意这里没有考虑链表中第一个和第二个元素相等的情况,这种情况默认认为是升序;
if(h1->next == NULL) flag1 = 0;
else if(h1->data < h1->next->data) flag1 =1;
if(h2->next == NULL) flag2 = 0;
else if(h2->data < h2->next->data) flag2 = 1;
if(flag1 != flag2) {
// 两个链表的升降序是不同的,需要先将其中一个反序
head1 = reverse(head1);
}
// 然后将两个链表进行合并
If(flag1) {
// 降序
While(h1 != NULL && h2 != NULL) {
if(h1->data <= h2->data) {
temp->next = h1;
temp = h1;
h1 = h1->next;
}else {
temp->next = h2;
temp = h2;
h2 = h2->next;
}
}
}else {
// 升序
While(h1 != NULL && h2 != NULL) {
if(h1->data >= h2->data) {
temp->next = h1;
temp = h1;
h1 = h1->next;
}else {
temp->next = h2;
temp = h2;
h2 = h2->next;
}
}
}
temp->next == h1? h1: h2;
free(head2);
return h3;
}
(37) 计算一个单链表的长度
判断链表是否为空,是否有环,是否是循环链表
(38) 数组和链表的区别
⑴ 空间:在存储相同多的元素时,链表需要多一倍的存储空间;
⑵ 访问:数组可以随机访问;链表只能够从头遍历到尾;
⑶ 顺序遍历时效率:当需要将一个数组和其对应的链表从头到遍历一遍时,数组的效率更高,因为在访问一个元素时,数组只需要一次内存的访问,而链表可能需要两次,这里的可能是很大可能,需要考虑那种链表的元素全都是连续存放,而cache刚好全都存进这些元素的情况。
⑷ 查找: 对于有序的数组,可以采用二叉查找法进行元素的查找,而有序的链表还是只能够从头到尾的遍历;
⑸ 删除:链表在删除一个元素,很快,只需要将需要删除的一个元素从链表中卸下来就可以了,而数组删除一个元素后,需要进行元素的迁移;
⑹ 插入:由于数组在一开始就必须确定大小,且其大小在确定以后一般不能够更改(当然,可以使用一个更大数组,将数组的元素全都复制过去,但这已经不是我们所说的大小更改了,数组地址都已近改了), 但是链表可以无限的插入新的元素。
操作系统的知识
(39) 进程调度算法
FCFS(先来先服务进程/作业调度算法):每次从就绪队列中选择一个最先进入该队列的进程,为它分配处理机。FCFS有利于长进程,而不利于短进程。
SP(J)F(短作业/进程优先调度算法):从就绪队列中选出一个估计运行时间最短的进程,为它分配处理机。优点:能够有效第降低作业的平均等待时间,提高系统的吞吐量。缺点:⑴ 该算法对长作业或长进程不利;⑵ 该算法完全没有考虑作业的紧迫程度,所以它不能够保证紧迫性作业会被及时处理;⑶ 由于作业或进程的运行时间都是根据用户自己提供的估计执行时间而定的,而用户可能会有意或无意地缩短作业或进程的估计运行时间。
高优先级调度算法:
基于时间片的轮转调度算法:
(40) 磁盘调度算法
FIFO
LRU
Clock
LFU
PBA
(41) 虚拟内存
(42)线程与进程的区别
⑴ 进程是资源分配和拥有的单位,每个进程都拥有代码区、数据区、寄存器值等等,所以进程很庞大,累赘;而线程可以同同一个进程下的其他线程共享进程的代码,数据区,而只需要拥有自己的指令指针寄存器和栈就可以轻量的运行;
⑵ 由于进程与进程是相互独立的,所以常常进程间的通信机制比进程间的同步机制使用的更频繁;而由于线程之间共享代码和数据区,所以线程更加注重线程之间的同步;
⑶ 进程间切换需要很大的开销,线程切换开销比较小,所以线程是处理器调度的基本单位。
⑷ 我的理解:进程是“静态的”,线程是动态的,当我们写好的源代码(程序)被编译连接转化为二进制可执行码时,它存放在硬盘上,当我们点击运行时,操作系统将该二进制文件加载到内存中,为它分配一块内存空间,初始化寄存器以及各个区,还没有运行,此时它叫做进程,当需要运行该内存中二进制时,需要生成一个线程或多个线程,来执行,这个在执行中的才叫做线程。
(43)线程同步的机制
(44)进程通信机制
(45)程序的内存分配情况
一个程序在编译运行的时候,它至少作为一个进程进行运行。在运行前,操作系统会为它分配内存空间,包括代码区、栈区、堆区、全局区、常量区。每个区存放的东西是不同的。
栈区:由编译器自动分配释放,存放函数的参数值,局部变量的值等。在栈中的数据是先进后出的。
堆区:一般由程序员分配释放,若程序员不释放,程序结束后由操作系统回收,但是一般都要程序员释放。存放的数据是动态生成的数据,比如new或malloc函数等
全局区(静态区)(static):全局变量和静态变量的存储时放在一块的,初始化了的全局变量和初始化了的静态变量存放在同一块区域,未初始化的全局变量和未初始化的静态变量存放在相邻的另一块区域。
常量区:常量存放在这里。如常量字符串,数字等,程序结束是由系统释放。
代码区:存放程序的二进制代码。
概率题:
(46)
一个不知道有多少条目的文件 每个条目一行 类似下面的结构
sdfgdfsgdfsgf
ertyrteyrtye
ytuityuityuityui
etrwtwetewtwt
.....
要求从头到尾只遍历一遍,等概率取其中100个条目,可以用rand()等系统函数时侯
和不能用任何系统调用时都怎么做?
假设文件的行数n大于100行(其实是废话,小于100行就没意义了)
预先申请一个string str[100],将前100行的记录都存取进去。
然后从第101行开始,假设当前是第K行,令m=rand() % k,如果m<100,就将str[m]的值修改为当前行内容,否则继续。最后str里面的内容就是取出的100个条目。
现在需要证明这种算法下,第k行被选中的概率为100/n。
A k>100的情况,第一次被选入到str中的概率为100/k,第2次没有被剔除来的概率为k/k+1,第三次为k+1/k+2,最后一次为n-1/n,则最后的概率为
(100/k) * (k/k+1) * (k+1/k+2) *.....*(n-2/n-1) * (n-1/n) = 100/n
B k<100,则从第101行开始,第一次不被剔除的概率是100/101,第二次101/102,最后一次n-1/n,则最后被选中的概率为
(100/101) * (101/102) * .........* (n-2/n-1) * (n-1/n) = 100/n
感觉这样是可以保证每一行被选中的概率是100/n,这个结论可以推广为n行选择m个条目
(47) 有一百个数,只让你取十次,每次取出一个数后,就剔除该元素,让你取出的这个十个数的概率一样?
每次去一个元素,然后下一次取到重复的话,就取它的下一个;
(48) 给你一个rand_m的函数,即产生(1,m)之间数的随机函数,如何构造一个函数rand_n来产生(1,n)之间的随机数。
Rand_n = m*(rand_m-1) + rand_m 这里的n需要满足 m<=n<m*m;
如果n比m*m大的话,则需要三部分了
Rand_n = m*m*(rand_m-1) +m*(rand_m-1) + rand_m
(49) 给你一个链表,要求从尾到头输出链表中的元素
(50) 有一个复杂链表,其中每个节点拥有三个域,数据域data,下一个节点next指针域,还有一个指针域pSibling指针链表中某一个节点或为NULL。
字符串题目
(51)将字符串转换为整数,将整数转换为字符串(atoi, itoa)
需要考虑的问题有:
① 需要使用unsigned long int 来存放结果,防止溢出;
② 需要判断字符串的前导空格符[包括' '和'\t'];
③ 需要判断正负号;
④ 需要判断开头数字是否合法;
⑤ 需要判断整数的进制
⑥ 需要在转换的过程中判断是否越界;
(52)找出字符串的最长子串,要求子串的所有字符相同
由于子串是连续的字符,所以我们可以不回头的判断,比如ai到aj是相同的字符,那么如果从aj+1开始不同了,那么下一次的开始处就是aj+1。
(53)求两个字符串的最长公共子串 LCS [动态规划的解法]
假设我们使用C[i, j]来表示字符串X0~iY0~j最长公共子串的长度,对于两个给定的子串来说,他们最长公共子串的长度时确定的,如果Xi和Yj不等的话,则最长的公共子串只可能在Xi-1,Yj或者Xi,Yj-1之间;从而又下面的式子
(54)去除一个字符串中相邻重复字符;
比如aabbcc --> abc; aaabbbcc --> abc; abccccdddd->abcd等
Char * DeDupChar(char** pChar) {
if(*pChar == NULL) {
Return NULL;
}
Char* newPChar = (char*)malloc(sizeof(char)*strlen(*pChar));
Char* tempPChar = *pChar, *pStr = newPChar;
Char curChar = *tempPChar;
*pStr++ = curChar;
tempPChar++;
While(*tempPChar != '\0') {
If(*tempPChar == curChar) {
tempPChar++;
}else {
*pStr++ = *tempPChar;
curChar = *tempPChar;
tempPChar++;
}
}
Return newPChar;
}
(55)在指定的字符串种删除某些字符
对需要删除的字符串建立一个hash表;
(56)判断一个字符串是否是一个回文字符串
(57)判断一个字符串是否可以通过移位另外一个字符串而得到
假如有两个字符串str1和str2,查看是str2能够通过移位str1来得到;例子,str1=AACD,str2 = CDAA,通过移位,是能够得到str2的,所以返回true;
具体解法是,通过在[str1,str1]这样的字符串中,匹配str2;上例就变成了是否在AACDAACD中存在字符串CDAA了,是就返回true,否则返回false
(58)strlen,strcpy函数的实现
特别是strcpy的拷贝函数
char* strcpy(char *DstStr, const char* SrcStr) {
assert((DstStr != NULL) && (SrcStr != NULL));
char* pDstStr = DstStr;
While((*DstStr++ = *SrcStr++) != '\0');
return pDstStr;
}