1、C/C++程序的内存分区
其实C和C++的内存分区还是有一定区别的,但此处不作区分:
1) 、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2) 、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3) 、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。
4) 、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
5) 、程序代码区—存放函数体的二进制代码。
栈区与堆区的区别:
1) 堆和栈中的存储内容:栈存局部变量、函数参数等。堆存储使用new、malloc申请的变量等;
2) 申请方式:栈内存由系统分配,堆内存由自己申请;
3) 申请后系统的响应:栈——只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢 出。
堆——首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第 一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表 中删除,并将该结点的空间分配给程序;
4) 申请大小的限制:Windows下栈的大小一般是2M,堆的容量较大;
5) 申请效率的比较:栈由系统自动分配,速度较快。堆使用new、malloc等分配,较慢;总结:栈区优势在处理效率,堆区优势在于灵活;
内存模型:自由区、静态区、动态区;
根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即:自由存储区,动态区、静态区。自由存储区:局部非静态变量的存储区域,即平常所说的栈;
动态区: 用new ,malloc分配的内存,即平常所说的堆;静态区:全局变量,静态变量,字符串常量存在的位置; 注:代码虽然占内存,但不属于c/c++内存模型的一部分;
2、快速排序的思想、时间复杂度、实现以及优化方法
1. 快速排序的三个步骤
(1) 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot);
(2) 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在 基准右边的元素都比基准大;
(3) 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。基准的选择:
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。
即:同一数组,时间复杂度最小的是每次选取的基准都可以将序列分为两个等长的;时间复杂度最大的是每次选择 的基准都是当前序列的最大或最小元素;
快排代码实现:
我们一般选择序列的第一个作为基数,那么快排代码如下:
void quicksort(vector<int> &v,int left, int right)
{
if(left < right)//false则递归结束
{
int key=v[left];//基数赋值 int low = left;
int high = right;
while(low < high) //当low=high时,表示一轮分割结束
{
while(low < high && v[high] >= key)//v[low]为基数,从后向前与基数比较
{
high--;
}
swap(v[low],v[high]);
while(low < high && v[low] <= key)//v[high]为基数,从前向后与基数比较
{
low++;
}
swap(v[low],v[high]);
}
//分割后,对每一分段重复上述操作quicksort(v,left,low-1); quicksort(v,low+1,right);
}
}
注:上述数组或序列v必须是引用类型的形参,因为后续快排结果需要直接反映在原序列中; 优化:
上述快排的基数是序列的第一个元素,这样的对于有序序列,快排时间复杂度会达到最差的o(n^2)。所以,优化方 向就是合理的选择基数。
常见的做法“三数取中”法(序列太短还要结合其他排序法,如插入排序、选择排序等),如下:
①当序列区间长度小于 7 时,采用插入排序;
②当序列区间长度小于 40 时,将区间分成2段,得到左端点、右端点和中点,我们对这三个点取中数作为基数;
③当序列区间大于等于 40 时,将区间分成 8 段,得到左三点、中三点和右三点,分别再得到左三点中的中数、中三点中的中数和右三点中的中数,再将得到的三个中数取中数,然后将该值作为基数。
具体代码只是在上一份的代码中将“基数赋值”改为①②③对应的代码即可:
int key=v[left];//基数赋值 if (right-left+1<=7) {
insertion_sort(v,left,right);//插入排序return;
}else if(right-left+1<=8){ key=SelectPivotOfThree(v,left,right);//三个取中
}else{
//三组三个取中,再三个取中(使用4次SelectPivotOfThree,此处不具体展示)
}
需要调用的函数:
void insertion_sort(vector<int> &unsorted,int left, int right) //插入排序算法
{
for (int i = left+1; i <= right; i++)
{
if (unsorted[i - 1] > unsorted[i])
{
int temp = unsorted[i]; int j = i;
while (j > left && unsorted[j - 1] > temp)
{
unsorted[j] = unsorted[j - 1]; j--;
}
unsorted[j] = temp;
}
}
}
int SelectPivotOfThree(vector<int> &arr,int low,int high)
//三数取中,同时将中值移到序列第一位
{
int mid = low + (high - low)/2;//计算数组中间的元素的下标
//使用三数取中法选择枢轴
if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]
{
swap(arr[mid],arr[high]);
}
if (arr[low] > arr[high])//目标: arr[low] <= arr[high]
{
swap(arr[low],arr[high]);
}
if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]
{
swap(arr[mid],arr[low]);
}
//此时,arr[mid] <= arr[low] <= arr[high] return arr[low];
//low的位置上保存这三个位置中间的值
//分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了
}
这里需要注意的有两点:
①插入排序算法实现代码;
②三数取中函数不仅仅要实现取中,还要将中值移到最低位,从而保证原分割函数依然可用。
3、IO模型——IO多路复用机制
IO模型有4中:同步阻塞IO、同步非阻塞IO、异步阻塞IO、异步非阻塞IO;IO多路复用属于IO模型中的异步阻塞IO模型,在服务器高性能IO构建中常常用到。
同步异步是表示服务端的,阻塞非阻塞是表示用户端,所以可解释为什么IO多路复用(异步阻塞)常用于服务器端的原因;
文件描述符(FD,又叫文件句柄):描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等属性)。具体来源:Linux内核将所有外部设备都看作一个文件来操作,对文件的操作都会调用内核提供的系统命令,返回一个fd(文件描述符)。
下面开始介绍IO多路复用:
(1) I/O多路复用技术通过把多个I/O的阻塞复用到同一个select、poll或epoll的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程。
(2) select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
(3) I/O多路复用的主要应用场景如下:
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字; 服务器需要同时处理多种网络协议的套接字;
(4) 目前支持I/O多路复用的系统调用有 select,poll,epoll,epoll与select的原理比较类似,但epoll作了很多重大改进,现总结如下:
①支持一个进程打开的文件句柄FD个数不受限制(为什么select的句柄数量受限制:select使用位域的方式来传递关心的文件描述符,因为位域就有最大长度,在Linux下是1024,所以有数量限制);
②I/O效率不会随着FD数目的增加而线性下降;
③epoll的API更加简单;
(5) 三种接口调用介绍:
①select函数调用格式: #include <sys/select.h> #include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval
*timeout)
//返回值:就绪描述符的数目,超时返回0,出错返回-1
②poll函数调用格式: # include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
③epoll函数格式(操作过程包括三个函数): #include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(6) 作用:一定程度上替代多线程/多进程,减少资源占用,保证系统运行的高效率; 更多细节待续……
4、实践中如何优化MySQL
四条从效果上第一条影响最大,后面越来越小。
① SQL语句及索引的优化
② 数据库表结构的优化
③ 系统配置的优化
④ 硬件的优化
5、什么情况下设置了索引但无法使用
① LIKE语句,模糊匹配
② OR语句
③ 数据类型出现隐式转化(如varchar不加单引号的话可能会自动转换为int型)
6、 如何设计一个高并发的系统
① 数据库的优化,包括合理的事务隔离级别、SQL语句优化、索引的优化;
② 使用缓存,尽量减少数据库 IO;
③ 分布式数据库、分布式缓存;
④ 服务器的负载均衡;
7、 两条相交的单向链表,如何求他们的第一个公共节点
①如果两个链表相交,则从相交点开始,后面的节点都相同,即最后一个节点肯定相同;
②从头到尾遍历两个链表,并记录链表长度,当二者的尾节点不同,则二者肯定不相交;
③尾节点相同,如果A长为LA,B为LB,如果LA>LB,则A前LA-LB个先跳过;
——更多如链表相关经典问题:求单向局部循环链表的入、将两个有序链表合并合成一个有序链表、链表逆序、求 倒数第K个节点,判断是否有环等。
8、overload、override、overwrite的介绍
(1) overload(重载),即函数重载:
①在同一个类中;
②函数名字相同;
③函数参数不同(类型不同、数量不同,两者满足其一即可);
④不以返回值类型不同作为函数重载的条件。
(2) override(覆盖,子类改写父类的虚函数),用于实现C++中多态:
①分别位于父类和子类中;
②子类改写父类中的virtual方法;
③与父类中的函数原型相同。
(3) overwrite(重写或叫隐藏,子类改写父类的非虚函数,从而屏蔽父类函数):
①与overload类似,但是范围不同,是子类改写父类;
②与override类似,但是父类中的方法不是虚函数。
9、什么是守护进程?
(1) 什么是守护进程?
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于
控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任 何终端所产生的信息所打断,其在执
行过程中的信息也不在任何终端上显示。
(2) 如何查看守护进程? 在终端敲:ps axj
从上图可以看出守护进行的一些特点:
守护进程基本上都是以超级用户启动( UID 为 0 )没有控制终端( TTY 为 ?)
终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)
10、 小端/大端机器
小端/大端的区别是指低位数据存储在内存低位还是高位的区别。其中小端机器指:数据低位存储在内存地址低位, 高位数据则在内存地址高位;大端机器正好相反。
当前绝大部分机器都是小端机器,就是比较符合人们逻辑思维的数据存储方式,比如intel的机器基本就都是小端机 器。
11、 长连接与短连接
(1) 就是TCP长连接和TCP短连接:
①TCP长连接:TCP长连接指建立连接后保持连接而不断开。若一段时间内没有数据传输,服务器会发送心跳包给客 户端,判断客户端是否还在线,叫做TCP长连接中的keep alive。一般步骤:连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
②TCP短连接:指连接建立并传输数据完成后,就断开连接。一般步骤:连接→数据传输→关闭连接;
③使用场景:长连接适合单对单通信且连接数不太多的情况;短连接适合连接数多且经常更换连接对象的;
(2) HTTP是什么连接:
①在HTTP/1.0中,默认使用的是短连接。但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的 HTTP协议,会在响应头有加入这行代码:
Connection:keep-alive
注意:此处的keep-alive和上述TCP长连接原理介绍中的keep alive不是一个意思:此处表示告知服务器本http请求是长连接模式,而TCP长连接中的keep alive表示对客户端的保活检测。
②http长连接并不是一直保持连接
http的长连接也不会是永久保持连接,它有一个保持时间如20s(从上一次数据传输完成开始计时),可以在不同的 服务器软件(如Apache)中设定这个时间,若超过该时间限制仍然无数据通信传输,服务器就主动关闭该连接。 注:实现长连接要客户端和服务端都支持长连接。
③http连接实质:http的长连接/短连接实质上就是TCP的长/短连接。
12、判断两棵树是否相等,请实现两棵树是否相等的比较,相等返回1,否则返回其他值,并说明算法 复杂度。
数据结构为:
typedef struct TreeNode
{
char c;
TreeNode *leftchild;
TreeNode *rightchild;
}TreeNode;
函数接口为:int CompTree(TreeNode* tree1,TreeNode* tree2);
注:A、B两棵树相等当且仅当RootA->c==RootB-->c,而且A和B的左右子树相等或者左右互换相等。递归方法:
bool CompTree(TreeNode *tree1, TreeNode *tree2)
{
if (tree1 == NULL && tree2 == NULL) return true;
if (tree1 == NULL || tree2 == NULL) return false;
if (tree1->c != tree2->c) return false;
if ( (CompTree(tree1->leftchild, tree2->leftchild) && CompTree(tree1->rightchild, tree2->rightchild)) || CompTree(tree1->leftchild, tree2->rightchild) && CompTree(tree1->rightchild, tree2->leftchild) ) return true;
}
时间复杂度:
在树的第0层,有1个节点,我们会进行1次函数调用;
在树的第1层,有2个节点,我们可能会进行4次函数调用; 在树的第2层,有4个节点,我们可能会进行16次函数调用;
......
在树的第x层,有2^x个节点,我们可能会进行(2^x)^2次函数调用; 所以假设总节点数为n,则算法的复杂度为O(n^2)。
13、如果有几千个session,怎么提高效率?
把session放到 redis 或 memcache 等此类内存缓存中或着把session存储在SSD硬盘上。session对应的文件有一个特点就是小,一般在几KB左右,
如果session以文件方式存储,如果并发数量级有几千个,
此时系统硬盘的随机IO早已成了系统中的最大瓶颈了,因为会话文件是存储在多个小文件中,映射到存储空间不是一段连续的地址范围
所以硬盘的随机读取能力显得非常重要,而觉机械硬盘的随机IO一般在100/iops上下, (IOPS (Input/Output Operations Per Second),即每秒进行读写(I/O)操作的次数) SSD固态硬盘可以达几百至上千,所以在这么高并发读写的情况下如果无条件用SSD固态盘 可以把session放到 redis 或 memcache 等内存缓存中,系统对内存的操作又是非常快的, 只要你的内存足够大,再多session并发速度一样不会慢。
14、顺序栈的表示和实现
顺序栈,即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top 指示栈顶元素在顺序栈中的位置。通常的习惯做法以top=0表示空栈。一般来说,在初始化设空栈时不应限定栈的最大容量。一个较合理的做法:先为栈分配一个基本容量,然后在应用过程中,当栈的空间不足在进行扩展。
#define STACK_INIT_SIZE 100
#define STACKINCREMENT 10
typedef struct SqStack
{
int *base; int *top;
int stacksize;
}SqStack;
int InitStack(SqStack &S)
{
S.base=(int *)malloc(STACK_INIT_SIZE*sizeof(int)); if(!S.base)
exit(0); S.top=S.base;
S.stacksize=STACK_INIT_SIZE; return 0;
}
int GetTop(SqStack S,int &e)
{
if(S.top==S.base) return 1;
e=*(S.top-1); return 0;
}
int Push(SqStack &S,int e)
{
if(S.top-S.base>=S.stacksize)
{
S.base=(int *)realloc(S.base,(S.stacksize+STACKINCREMENT)*sizeof(int)); if(!S.base)
return 1; S.top=S.base+S.stacksize; S.stacksize+=STACKINCREMENT;
}
*S.top=e; S.top++; return 0;
}
int Pop(SqStack &S,int &e)
{
if(S.top==S.base) return 1;
e=*S.top; return 0;
}
int StackEmpty(SqStack S)
{
if(S.top==S.base) return 1;
return 0;
}
int DestroyStack(SqStack &S)
{
free(S.base); S.top=NULL;
S.base=S.top; return 0;
}