百度C++研发面试题汇总

目录

 

算法

剑指offer 11、旋转数组的最小数字

打印字符串中的所有回文串(要时间复杂度o(n)-希尔排序)

迷宫寻路(dfs和bfs的区别,优缺点)

BFS

DFS

dfs和bfs的区别,优缺点

字符串复制

多线程单例模式

求幂,优化

判断回文,优化

实现快速排序

实现非递归后序遍历二叉树

前序遍历

中序遍历

后序遍历

大数的斐波那契,除留余数

先升序后降序的,找值最快的方法

abcd 10000 一共有多少种可能

插入排序

统计二叉树第k层的结点数目

如何计算整个链表中节点数量

实现String

海量数据中找出出现次数最多的前10个URL

topk

面向对象,写三个class,动物、狗、猫,你自己发挥(访问控制权限符、virtual、多态、虚析构巴拉巴拉,然后解释了一下)

希尔排序

699个结点的完全二叉树,有叶子节点多少个?

大鱼吃小鱼

判断一个链表中是否有环

链表反转

判断括号是否有效

C++基础

externC

extern "C"的含义

应用场合

虚函数使用范围及不同情况

1、什么是虚函数

2、什么时候用到虚函数

3、虚函数/接口/抽象函数

多态种类和区别

 

静态多态和动态多态的区别

C++内存分配方式,静态变量在哪儿

C++中的static关键字

面向过程中的static

面向对象中的static

指针引用

智能指针

auto_ptr

unique_ptr

share_ptr

weak_ptr

迭代器使用(map set vector list)

vector与list元素个数相同,遍历一遍,哪个快

红黑树和平衡二叉树区别

哈希表

STL的allocator

new和malloc的内存对齐

特化,偏特化

虚函数存在哪个内存区

数据库

mysql底层索引及为什么用这个索引

为什么哈希表、完全平衡二叉树、B树、B+树都可以优化查询,为何Mysql独独喜欢B+树?

哈希表

完全平衡二叉树

B树

B+树

B+树到底有什么优势呢?

mysql 事务ACID

mysql事务

ACID特性

mysql隔离级别及实现

查询是否使用索引(explain)

sql优化

Mysql MyISAM和InnoDB的区别

MyISAM 存储引擎的特点

InnoDB 存储引擎的特点

MyISAM 和 InnoDB 存储引擎的对比

主键索引和非主键索引的区别

最左前缀

MYSQL MVCC

什么是脏读?幻读?不可重复读?Innodb怎么解决幻读?

计算机网络

https和http

SSL/TLS 握手过程

长连接短连接

tcp拥塞控制

UDP和TCP区别,分别怎么处理丢包

TCP与UDP基本区别

TCP/UDP分别处理丢包方法

TCP/UDP应用场景

UDP怎么做简单的可靠传输

七层协议/TCP/IP网络模型

三次握手/四次挥手/三次握手属于七层协议里面的哪一层

三次握手

四次挥手

TCP 有哪些字段

为什么要三次握手和四次挥手?

TIMEWAIT

线程间同步的方式,进程间同步的方式和进程间通信的方式

浏览器打开一个网页经历了怎样的过程

操作系统

死锁

死锁怎么解决?

进程、线程通信

进程间通信方式

线程间通信

线程进程协程

一、概念

二、区别:

Select,epoll,epoll两种触发方式

Select

epoll

epoll两种触发方式

线程池

调用malloc时会立即分配物理内存吗?页表中一定会对应物理页框吗?swap交换空间

CPU高速缓存的逻辑

信号量与条件变量的区别

new和malloc的区别/delete和free的区别


算法

剑指offer 11、旋转数组的最小数字

思路:从头到尾遍历数组一次,我们就能找出最小的元素。这种思路的时间复杂度显然是O(n)。但是这个思路没有利用输入的旋转数组的特性,肯定达不到面试官的要求。我们注意到旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素都大于或者等于后面子数组的元素。我们还注意到最小的元素刚好是这两个子数组的分界线。在排序的数组中我们可以用二分查找法实现O(logn)的查找

Step1.和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。

Step2.接着我们可以找到数组中间的元素:如果该中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于该中间元素的后面。我们可以把第一个指针指向该中间元素,这样可以缩小寻找的范围。移动之后的第一个指针仍然位于前面的递增子数组之中。如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。

Step3.接下来我们再用更新之后的两个指针,重复做新一轮的查找。

public static int GetMin(int[] numbers)
    {
        if (numbers == null || numbers.Length <= 0)
        {
            return int.MinValue;
        }

        int index1 = 0;
        int index2 = numbers.Length - 1;
        // 把indexMid初始化为index1的原因:
        // 一旦发现数组中第一个数字小于最后一个数字,表明该数组是排序的
        // 就可以直接返回第一个数字了
        int indexMid = index1;

        while (numbers[index1] >= numbers[index2])
        {
            // 如果index1和index2指向相邻的两个数,
            // 则index1指向第一个递增子数组的最后一个数字,
            // index2指向第二个子数组的第一个数字,也就是数组中的最小数字
            if (index2 - index1 == 1)
            {
                indexMid = index2;
                break;
            }
            indexMid = (index1 + index2) / 2;
            // 特殊情况:如果下标为index1、index2和indexMid指向的三个数字相等,则只能顺序查找
            if (numbers[index1] == numbers[indexMid] && numbers[indexMid] == numbers[index2])
            {
                return GetMinInOrder(numbers, index1, index2);
            }
            // 缩小查找范围
            if (numbers[indexMid] >= numbers[index1])
            {
                index1 = indexMid;
            }
            else if (numbers[indexMid] <= numbers[index2])
            {
                index2 = indexMid;
            }
        }

        return numbers[indexMid];
    }

    public static int GetMinInOrder(int[] numbers, int index1, int index2)
    {
        int result = numbers[index1];
        for (int i = index1 + 1; i <= index2; ++i)
        {
            if (result > numbers[i])
            {
                result = numbers[i];
            }
        }

        return result;
    }

打印字符串中的所有回文串(要时间复杂度o(n)-希尔排序)

描述
    给定一个字符串,输出所有长度至少为2的回文子串。
    回文子串即从左往右输出和从右往左输出结果是一样的字符串,
    比如:abba,cccdeedccc都是回文字符串。

分析:

该题目输出格式要求比较特别:子串长度小的优先输出,若长度相等,则出现位置靠左的优先输出。

所以,这里分如下几个步骤来完成任务:

1、枚举子串的所有可能的长度 for(len=2;len<=n;len++)

2、当长度确定为len时,枚举所有长度为len的子串的开始点。

        maxBegin=n-len;
        for(begin=0;begin<=maxBegin;begin++)

3、当开始点和长度明确时,可以遍历该子串并判断其是否回文串。

            j=begin+len-1;
            for(i=begin;i<j;i++,j--)

#include<stdio.h>
#include<string.h>
int main(int argc, char *argv[])
{
    char a[505];
    int n,len,begin,maxBegin,i,j;
    freopen("29.in","r",stdin);
    scanf("%s",a);
    n=strlen(a);
    for(len=2;len<=n;len++)//枚举子串的所有可能的长度
    {
        maxBegin=n-len;
        for(begin=0;begin<=maxBegin;begin++)//枚举子串的开始点 
        {
            j=begin+len-1;
            for(i=begin;i<j;i++,j--) //遍历当前子串(a[i]~a[begin+len-1]),判断是否回文串 
            {
                if(a[i]!=a[j]) break;
            }
            if(i>=j)//是回文串
            {
                j=begin+len-1;
                for(i=begin;i<=j;i++) printf("%c",a[i]);
                printf("\n");
            }
        }
    } 
    return 0;
}

迷宫寻路(dfs和bfs的区别,优缺点)

参考:https://blog.csdn.net/Dog_dream/article/details/80270398

BFS


宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
如上所示我要做的就是搜索整张,定义一个二维数组visit, visit[x][y]判断坐标x,y是否被访问,初始化visit为0都没有被访问;定义一个结构体point里面的参数有x,y,dis;其中x,y表示坐标,dis表示出发点到该节点的步数;
bfs函数操作:
1,将节点添加到队列里面;
2,从队头取出节点将其访问状态设为1,判断其上下左右四个节点将符合要求的节点添加到队列中;
3,重复1,2操作直到从队列中取出的节点终点返回其dis;

#include<stdio.h>
#include<iostream>
#include<stdlib.h>
#include<string>
#include<string.h>
#include<algorithm>
#include<stack>
#include<queue>
#include<iterator>
using namespace std;
#define MAX   10

int x, y, a, b;
struct  point
{
	int x, y, dis;//x坐标y坐标步数
};
int fx[4] = { -1,1,0,0 }, fy[4] = { 0,0,-1,1 };
int bfs(int x, int y,int maze[][9])
{
	queue<point> myque;
	point tp;
	tp.x = x;tp.y = y;tp.dis = 0;//初始化开始节点dis设为0
	myque.push(tp);
	while (!myque.empty())
	{
		tp = myque.front();myque.pop();//从队头取节点
		if (tp.x == a && tp.y == b) { return tp.dis; }//判断是否到达目的地
		for (int i = 0;i < 4;i++)
		{
			if (tp.x + fx[i] < 9 && tp.x + fx[i] >= 0 && tp.y + fy[i] < 9 &&
				 tp.y + fy[i] >= 0 && maze[tp.x + fx[i]][tp.y + fy[i]] == 0)
			{
				point tmp;
				tmp.x = tp.x + fx[i];
				tmp.y = tp.y + fy[i];
				tmp.dis = tp.dis + 1;
				maze[tmp.x][tmp.y] = 1;//添加进队列就将该位置设为1
				myque.push(tmp);
			}
		}
	}
}

int main()
{
	int t;
	cin >> t;
	while (t--)
	{
		int maze[9][9] =
		{ { 1,1,1,1,1,1,1,1,1 },
		{ 1,0,0,1,0,0,1,0,1 },
		{ 1,0,0,1,1,0,0,0,1 },
		{ 1,0,1,0,1,1,0,1,1 },
		{ 1,0,0,0,0,1,0,0,1 },
		{ 1,1,0,1,0,1,0,0,1 },
		{ 1,1,0,1,0,1,0,0,1 },
		{ 1,1,0,1,0,0,0,0,1 },
		{ 1,1,1,1,1,1,1,1,1 },
		};
		cin >> x >> y >> a >> b;
		cout << bfs(x, y, maze) << endl;

	}
	return 0;
}

DFS


深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.
顾名思义DFS就是从一个节点出发直到不能访问然后回转到上一层 也就是所说的 回溯+递归 实现方法就是从开始节点出发递归其四周只有满足要求就调用函数进行递归最后返回;所以我设置了两个数组dis,visit;dis存放步数,visit判断是否被访问;
 

#include<stdio.h>
#include<iostream>
#include<stdlib.h>
#include<string>
#include<string.h>
#include<algorithm>
#include<stack>
#include<queue>
#include<iterator>
using namespace std;
#define MAX   10
int maze[9][9] =
{ { 1,1,1,1,1,1,1,1,1 },
{ 1,0,0,1,0,0,1,0,1 },
{ 1,0,0,1,1,0,0,0,1 },
{ 1,0,1,0,1,1,0,1,1 },
{ 1,0,0,0,0,1,0,0,1 },
{ 1,1,0,1,0,1,0,0,1 },
{ 1,1,0,1,0,1,0,0,1 },
{ 1,1,0,1,0,0,0,0,1 },
{ 1,1,1,1,1,1,1,1,1 },
};

int x, y, a, b, dis[MAX][MAX],visit[MAX][MAX];
int dfs(int x, int y);
int next(int a,int b,int x, int y)
{
	if (dis[x][y] > dis[a][b] + 1) //如果小于就要从这个点从新遍历
	{
		dis[x][y] = dis[a][b] + 1;//更新其值
		visit[x][y] = 0;//将状态设为可访问   这步比较重要
	}
	if (!visit[x][y]) { dfs(x, y); }
	return 0;
}
// 深度优先
int dfs(int x, int y)
{
	visit[x][y] = 1;
	if (x + 1 < 9 && maze[x + 1][y] == 0) //上
	{ 
		next(x, y, x + 1, y);
	}
	if (x - 1 >=0 && maze[x - 1][y] == 0) //下
	{
		next(x, y, x - 1, y);
	}
	if (y + 1 < 9 && maze[x][y + 1] == 0) //左
	{
		next(x, y, x, y+1);
	}
	if (y - 1 >=0 && maze[x][y - 1] == 0) //右
	{
		next(x, y, x, y-1);
	}
	return 0;
}
int main()
{
	int t;
	cin >> t;
	while (t--)
	{
		memset(dis, 1, sizeof(dis));//初始化将dis的值设的比较大
		memset(visit, 0, sizeof(visit));//初始化visit将所有值设为0
		cin >> x >> y >> a >> b;   
		dis[x][y] = 0;
		dfs(x, y);
		cout << dis[a][b] << endl;
	}
	return 0;
}

dfs和bfs的区别,优缺点

一般来说是BFS比较快的吧。因为没有递归,runtime_error一般就是内存溢出,就是越界了!   BFS一般用来搜索最短路径最好,DFS用来搜索能不能到达目的地之类的。

BFS与DFS的讨论:BFS:这是一种基于队列这种数据结构的搜索方式,它的特点是由每一个状态可以扩展出许多状态,然后再以此扩展,直到找到目标状态或者队列中头尾指针相遇,即队列中所有状态都已处理完毕。

DFS:基于递归的搜索方式,它的特点是由一个状态拓展一个状态,然后不停拓展,直到找到目标或者无法继续拓展结束一个状态的递归。

优缺点:

BFS:对于解决最短或最少问题特别有效,而且寻找深度小,但缺点是内存耗费量大(需要开大量的数组单元用来存储状态)。

DFS:对于解决遍历和求所有问题有效,对于问题搜索深度小的时候处理速度迅速,然而在深度很大的情况下效率不高

总结:不管是BFS还是DFS,它们虽然好用,但由于时间和空间的局限性,以至于它们只能解决数据量小的问题。
 

字符串复制

#include<iostream>
#include<stdlib.h>
using namespace std;
 
char *str_cpy(char *dest,char s[])
{
	char *p=s;
	char *q=dest;
	int m=strlen(s);
	while(m--!=0)
	{
		*q++=*p++;
	}
	*q='\0';
	return dest;
}
int main()
{
	char a[100];
	cout<<"please input a:";
	gets_s(a);
	cout<<"a:"<<a<<endl;
 
	char *p=(char*)malloc(strlen(a)+1);
	str_cpy(p,a);
	cout<<"p:"<<p<<endl;
	system("pause");
	return 0;
}

多线程单例模式

参考:https://zhuanlan.zhihu.com/p/37469260

单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

定义一个单例类:

  1. 私有化它的构造函数,以防止外界创建单例类的对象;
  2. 使用类的私有静态指针变量指向类的唯一实例;
  3. 使用一个公有的静态方法获取该实例
class Singleton
{
private:
	static Singleton* instance;
private:
	Singleton() {};
	~Singleton() {};
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
public:
	static Singleton* getInstance() 
        {
		if(instance == NULL) 
			instance = new Singleton();
		return instance;
	}
};

// init static member
Singleton* Singleton::instance = NULL;

求幂,优化

 参考:https://blog.csdn.net/weixin_44048823/article/details/89348856

#include <iostream>

using namespace std;

//利用二进制求x的n次幂,x,n均为整数;只考虑n的大小
//例如求解2 ^ 10 那么10转化为二进制数字为1010 对应的位上分别为 8 4 2 1
//2 ^ 10 可以转化为 2 ^ 8 * 2 ^ 2
double Pow(int x, int n)
{
    int base = x;
    double result = 1;
    int y;
    if (n == 0)
        return 1;
    else if(n < 0)
        y = -n;
    else
        y = n;    

    while (y != 0)
    {
        //二进制的某一位是1,则乘以现在的base
        if (y & 1)
            result *= base;
        //base平方
        base *= base;
        //将指数右移1位
        y >>= 1;
    }
    if(n > 0)
        return result;
    else
        return double(1.0/result);
}

int main(int argc, char const *argv[])
{
    double r = Pow(5,-2);
    cout << r << endl;
    return 0;
}

判断回文,优化

参考:https://blog.csdn.net/duan19920101/article/details/51481348

给定一个字符串,如何判断这个字符串是否是回文串?

思路一:直接在字符串的首尾两端各放置一个指针*front和*back,然后开始遍历整个字符串,当*front不再小于*back时完成遍历。在此过程中,如果出现二者的值不相等,那么久表示不是回文串;如果两个指针指向的字符始终相等就表示该字符串是回文字符串。

时间复杂度:O(n)

思路二:先使用快慢指针确定出字符串的中间位置,然后分别使用两个指针从开中间位置开始向相反的方向扫描,知道遍历完整个字符串。


时间复杂度:O(n)

找中间位置的方法:

1、快慢指针;

2、一种有效的计算方法

//思路一
#include <iostream>
 
using namespace std;
 
//*s为字符串,n为字符串的长度
bool IsPalindrome(char *str, int n)
{
	//指向字符串首尾的指针
	char *front = str;
	char *back = str + n - 1;
 
	if(str==NULL||n<1)
	{
		return false;
	}
	while (front<back)
	{
		if (*front != *back)
		{
			return false;
		}
		front++;
		back--;
	}
	
	return true;	
}
 
int main( )
{
	char str[] = "abba";
	int n = strlen(str);
	bool sign;
	sign = IsPalindrome(str, n);
	if (sign == true)
	{
		cout << "此字符串是回文字符串"<<endl;
	}
	else
	{
		cout << "此字符串不是回文字符串" << endl;
	}
 
	return 0;
}
	
//思路二
#include <iostream>
 
using namespace std;
 
//*s为字符串,n为字符串的长度
bool IsPalindrome(char *str, int n)
{
	//指向字符串首尾的指针
	char *first;
	char *second;
 
	if (str == NULL || n<1)
	{
		return false;
	}
	//m定位到字符串的中间位置
	int m = ((n >> 1) - 1) >= 0 ? (n >> 1) - 1 : 0;
	first = str + m;
	second= str + n - 1 - m;
	while (first>str)
	{
		if (*first!= *second)
		{
			return false;
		}
		first--;
		second++;
	}
	
	return true;	
}
 
int main( )
{
	char str[] = "abcgba";
	int n = strlen(str);
	bool sign;
	sign = IsPalindrome(str, n);
	if (sign == true)
	{
		cout << "此字符串是回文字符串"<<endl;
	}
	else
	{
		cout << "此字符串不是回文字符串" << endl;
	}
 
	return 0;
}
	
 

实现快速排序

参考:https://blog.csdn.net/qq_27262727/article/details/104053863

#include<iostream>
 
using namespace std;
 
const int N = 1000010;
 
int n;
int q[N];
 
//快排模版
void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;//判断边界,如果输入只有一个数或者没有数 就直接return
 
    int x = q[(l + r) / 2], i = l - 1, j = r + 1;//取分界点x(中间点),i和j是指针
    while (i < j)//循环迭代,交换,调整区间
    {
        do i ++; while (q[i] < x);//循环直到q[i]>x
        do j --; while (q[j] > x);//循环直到q[i]<=x
        //如果循环之后,指针i和j没有相遇,交换两个数
        if (i < j) swap(q[i], q[j]);
    }
 
    quick_sort(q, l, j);
    quick_sort(q , j + 1, r);
}
 
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
 
    quick_sort(q, 0, n-1);
 
    for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
 
    return 0;
}

实现非递归后序遍历二叉树

参考:https://blog.csdn.net/MBuger/article/details/70186143

思路:利用栈的先入后出的特性,把结点按照前中后序的要求存入栈,再按照需要从栈中弹出。

前序遍历

大体思路是给出一个栈,从根节点开始向左访问每个结点并把它们入栈,直到左孩子全被访问,此时弹出一个结点以和上面同样的方式访问其右孩子。直到栈空。

vector<int> NPreOrder(TreeNode* root)
{
    vector<int> result;
    stack<TreeNode*> s;

    if (root == NULL)
        return result;

    while (root || !s.empty())
    {//结束遍历的条件是root为空且栈为空
        while(root)
        {//找到最左结点,并把路径上的所有结点一一访问后入栈
            s.push(root);
            result.push_back(root->val);
            root = root->left;
        }
        root = s.top();//取栈顶结点
        s.pop();//弹出栈顶结点
        root = root->right;//左和中都访问了再往右访问
    }
    return result;
}

中序遍历

中序遍历和前序遍历大同小异,只是访问元素的时间不同,中序遍历访问是在元素出栈的时候访问,而前序遍历是在元素入栈的时候访问。

vector<int> NInOrder(TreeNode* root)
{
    vector<int> result;
    stack<TreeNode*> s;

    if (root == NULL)
        return result;

    while (root || !s.empty())
    {
        while (root)
        {
            s.push(root);
            root = root->left;
        }
        root = s.top();
        result.push_back(root->val);
        s.pop();
        root = root->right;
    }
    return result;
}

后序遍历

后序遍历相对复杂一点,因为后序遍历访问一个结点的时候需要满足两个条件,一是该结点右孩子为空,二是该结点的右孩子已经被访问过,这两个条件满足一个则表示该结点可以被访问。

vector<int> PostOrder(TreeNode* root)
{
    vector<int> result;
    stack<int> s;
    TreeNode* cur = root;
    TreeNode* pre = NULL;

    if (root == NULL)
        return result;

    while (cur)
    {//走到最左孩子
        s.push(cur);
        cur = cur->left;
    }

    while (!s.empty())
    {
        cur = s.top();
        if (cur->right == NULL || cur->right == pre)
        {//当一个结点的右孩子为空或者被访问过的时候则表示该结点可以被访问
            result.push_back(cur->val);
            pre = cur;
            s.pop();
        }
        else
        {//否则访问右孩子
            cur = cur->right;
            while (cur)
            {
                s.push(cur);
                cur = cur->left;
            }
        }
    }
    return result;
}

大数的斐波那契,除留余数

#include<stdio.h>
int Fib(int n)

{
    if(n==1||n==2)
         return 1;
    else
        return (Fib(n-1)+Fib(n-2))%10007;
}

int main()
{
    int n=0;
    scanf("%d",&n);
    printf("%d\n",Fib(n));  
    return 0;
}

先升序后降序的,找值最快的方法

折半查找变种算法实现

#include <iostream>
 
int findMax(int* a, int l, int r)
{
	int m = (l + r) / 2;
 
	if(a[m] > a[m - 1] && a[m] > a[m + 1])
	{
		return a[m];
	}
	else if(a[m] > a[m - 1] && a[m] < a[m + 1])
	{
		return findMax(a, m + 1, r);
	}
	else
	{
		return findMax(a, l, m - 1);
	}
}
 
int main()
{
	int a[] = {1,2,3,4,5,8,9,22,6,5,4,1};
 
	int res = findMax(a, 0, 11);
 
	return 0;
 
}

abcd 10000 一共有多少种可能

参考:https://blog.csdn.net/morewindows/article/details/7370155

给定字符串S[0…N-1],设计算法,枚举S的全排列。如:123,全排列就是:123,132,213,231,312,321

全排列就是从第一个数字起每个数分别与它后面的数字交换。

//全排列的递归实现
#include <stdio.h>
#include <string.h>
void Swap(char *a, char *b)
{
	char t = *a;
	*a = *b;
	*b = t;
}
//k表示当前选取到第几个数,m表示共有多少数.
void AllRange(char *pszStr, int k, int m)
{
	if (k == m)
	{
		static int s_i = 1;
		printf("  第%3d个排列\t%s\n", s_i++, pszStr);
	}
	else
	{
		for (int i = k; i <= m; i++) //第i个数分别与它后面的数字交换就能得到新的排列
		{
			Swap(pszStr + k, pszStr + i);
			AllRange(pszStr, k + 1, m);
			Swap(pszStr + k, pszStr + i);
		}
	}
}
void Foo(char *pszStr)
{
	AllRange(pszStr, 0, strlen(pszStr) - 1);
}
int main()
{
	printf("         全排列的递归实现\n");
	printf("  --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
	char szTextStr[] = "123";
	printf("%s的全排列如下:\n", szTextStr);
	Foo(szTextStr);
	return 0;
}
//全排列的非递归实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Swap(char *a, char *b)
{
	char t = *a;
	*a = *b;
	*b = t;
}
//反转区间
void Reverse(char *a, char *b)
{
	while (a < b)
		Swap(a++, b--);
}
//下一个排列
bool Next_permutation(char a[])
{
	char *pEnd = a + strlen(a);
	if (a == pEnd)
		return false;
	char *p, *q, *pFind;
	pEnd--;
	p = pEnd;
	while (p != a)
	{
		q = p;
		--p;
		if (*p < *q) //找降序的相邻2数,前一个数即替换数
		{
			//从后向前找比替换点大的第一个数
			pFind = pEnd;
			while (*pFind <= *p)
				--pFind;
			//替换
			Swap(pFind, p);
			//替换点后的数全部反转
			Reverse(q, pEnd);
			return true;
		}
	}
	Reverse(p, pEnd);//如果没有下一个排列,全部反转后返回true
	return false;
}
int QsortCmp(const void *pa, const void *pb)
{
	return *(char*)pa - *(char*)pb;
}
int main()
{
	printf("         全排列的非递归实现\n");
	printf("  --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
	char szTextStr[] = "abc";
	printf("%s的全排列如下:\n", szTextStr);
	//加上排序
	qsort(szTextStr, strlen(szTextStr), sizeof(szTextStr[0]), QsortCmp);
	int i = 1;
	do{
		printf("第%3d个排列\t%s\n", i++, szTextStr);
	}while (Next_permutation(szTextStr));
	return 0;
}

插入排序

#include "stdafx.h"
#include<iostream>
using namespace std;
void InsertSort(int a[], int n)
{
    for (int j = 1; j < n; j++)
    {
        int key = a[j]; //待排序第一个元素
        int i = j - 1;  //代表已经排过序的元素最后一个索引数
        while (i >= 0 && key < a[i])
        {
            //从后向前逐个比较已经排序过数组,如果比它小,则把后者用前者代替,
            //其实说白了就是数组逐个后移动一位,为找到合适的位置时候便于Key的插入
            a[i + 1] = a[i];
            i--;
        }
        a[i + 1] = key;//找到合适的位置了,赋值,在i索引的后面设置key值。
    }
}
void main() {
    int d[] = { 12, 15, 9, 20, 6, 31, 24 };
    cout << "输入数组  { 12, 15, 9, 20, 6, 31, 24 } " << endl;
    InsertSort(d,7);
    cout << "排序后结果:";
    for (int i = 0; i < 7; i++)
    {
        cout << d[i]<<" ";
    }
 
}

统计二叉树第k层的结点数目

参考:https://blog.csdn.net/qq_40550018/article/details/83579158

用子问题递推的方法,我们可以用第k层左子树的节点数+第K层右子树的节点数。
终止条件:空树返回0,如果查的是第一层的节点数,返回1.

 int GetKLevelSize(BNode *root, int k)
{
	assert(k > 0);
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return GetKLevelSize(root->left, k - 1)+ GetKLevelSize(root->right, k - 1);
}

参考链接得细看,包括了二叉树结点个数、叶子节点个数、第K层节点数、高度求法。

如何计算整个链表中节点数量

#ifndef C466_H
#define C466_H
#include<iostream>
using namespace std;
/*
* Count how many nodes in a linked list.
* 
* Example
* Given 1->3->5, return 3.
*/
class ListNode{
public:
    int val;
    ListNode *next;
    ListNode(int val){
        this->val = val;
        this->next = NULL;
    }
};
class Solution {
public:
    /*
    * @param head: the first node of linked list.
    * @return: An integer
    */
    int countNodes(ListNode * head) {
        // write your code here
        if (head == NULL)
            return 0;
        ListNode *p = head;
        int len = 0;
        while (p != NULL)
        {
            len++;
            p = p->next;
        }
        return len;
    }
};
#endif

实现String

参考:https://coolshell.cn/articles/10478.html

C++ 的一个常见面试题是让你实现一个 String 类,限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源。具体来说:

  1. 能像 int 类型那样定义变量,并且支持赋值、复制。
  2. 能用作函数的参数类型及返回类型。
  3. 能用作标准库容器的元素类型,即 vector/list/deque 的 value_type。(用作 std::map 的 key_type 是更进一步的要求,本文从略)。

换言之,你的 String 能让以下代码编译运行通过,并且没有内存方面的错误。

#include <utility>
#include <string.h>

class String
{
 public:
  String()
    : data_(new char[1])
  {
    *data_ = '\0';
  }

  String(const char* str)
    : data_(new char[strlen(str) + 1])
  {
    strcpy(data_, str);
  }

  String(const String& rhs)
    : data_(new char[rhs.size() + 1])
  {
    strcpy(data_, rhs.c_str());
  }
  /* Delegate constructor in C++11
  String(const String& rhs)
    : String(rhs.data_)
  {
  }
  */

  ~String()
  {
    delete[] data_;
  }

  /* Traditional:
  String& operator=(const String& rhs)
  {
    String tmp(rhs);
    swap(tmp);
    return *this;
  }
  */
  String& operator=(String rhs) // yes, pass-by-value
  {
    swap(rhs);
    return *this;
  }

  // C++ 11
  String(String&& rhs)
    : data_(rhs.data_)
  {
    rhs.data_ = nullptr;
  }

  String& operator=(String&& rhs)
  {
    swap(rhs);
    return *this;
  }

  // Accessors

  size_t size() const
  {
    return strlen(data_);
  }

  const char* c_str() const
  {
    return data_;
  }

  void swap(String& rhs)
  {
    std::swap(data_, rhs.data_);
  }

 private:
  char* data_;
};

海量数据中找出出现次数最多的前10个URL

#include<iostream>
#include<string>
#include<map>
#include<vector>
using namespace std;

int main(void)
{
    //海量数据
    string a[5]={"ab","b","ccc","ab","ccc"};
    int n=sizeof(a)/sizeof(a[0]);
    cout<<n<<endl;
    vector<string> vs(a, a+n);
    //哈希统计频率
    map<string,int> mp;
    for(int i=0;i<n;i++)
    {
        if(mp.find(a[i])!=mp.end())
        {
            mp[vs[i]]++;
        }
        else
        {
            mp[vs[i]]=1;
        }
    }
    //对字符串按出现频率排序
    multimap<int,string> multimp;
    map<string,int>::iterator it;
    for(it=mp.begin();it!=mp.end();it++){
        multimp.insert(pair<int,string>(it->second,it->first));
        //multimp.insert(it->second,it->first);
    }
    //输出出现频率最高的两个字符串
    multimap<int,string>::iterator mit=multimp.end();
    for(int i=1;i<=2;i++)
    {
        mit--;
        cout<<mit->second<<endl;
    }
}

topk

#define NUM 10
typedef int ELEM;
void heap(ELEM a[],int left,int right)
{
    if (left >= right) 
            return ;
    int r = left;//指向当前的根结点
    int LChild = 2*r+1;//指向当前跟结点的左孩子
    int Child = LChild;

    ELEM temp = a[r];//记录当前根结点元素

    //开始逐渐向下调整
    while(LChild <= right)
    {
        if(LChild < right && a[LChild] > a[LChild+1])
                    Child = LChild + 1;//Child指向子节点中最小的那个
        if(temp > a[Child]) 
        {
                a[r] = a[Child];                    
                r = Child;//重新调整跟结点指向
                LChild = 2*r+1;//重新调整左孩子指向
                Child = LChild;//重新调整孩子指向
        }
        else
            break;
    }
    a[r] = temp;
    return;
}
int main(int argc,char **argv) 
{   
    if(argc < 2)
    {
        printf("参数数量不够!");
        return 0;
    }   
    int i;
    int k = atoi(argv[1]);
    ELEM a[NUM] = {2,5,3,1,6,13,15,1859,131,13};

    ELEM* b = (ELEM*)malloc(k*sizeof(ELEM));    
    memset(b,0,k*sizeof(ELEM));

    //核心部分
    for(i = 0;i < NUM;i++)
    {
            if(a[i] > b[0])
            {
                    b[0] = a[i];
                    heap(b,0,k-1);
            }
    }

    //如果还要求最大的k个数有序,将数组b(堆)排序即可.
    printf("a中最大的k个数:");
    for (i = 0;i < k;i++)
        printf("%d ",b[i]);     

    free(b);
    b = NULL;

    return 0;
}

面向对象,写三个class,动物、狗、猫,你自己发挥(访问控制权限符、virtual、多态、虚析构巴拉巴拉,然后解释了一下)

#include <iostream>
    using namespace std;
 
    class Base
    {
       public:
           virtual void Print() = 0;
           virtual ~Base(){}
    };
 
    class child_1 : public Base
    {
       public:
        void Print()
        {
            cout << "child_1 Print function" << endl;
        }
        ~child_1()
        {
            cout << "child_1 destructor function" << endl;
        }
    };
 
    class child_2: public Base
    {
        public:
            void Print()
            {
                cout << "child_2 Print function" << endl;
            }
            ~child_2()
            {
                cout << "child_2 destructor function" << endl;
            }
    };
 
    int main()
    {
        Base *p = new child_1; //父类指针指向子类对象
        p->Print();
        delete p;  //记住释放,否则内存泄露
        p = new child_2;
        p->Print();
        delete p;
        p = NULL;
        return 0;
    }

希尔排序

希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
下面给出严格按照定义来写的希尔排序:

//粗糙版本
void shellsort1(int a[], int n)
{
    int i, j, gap;

    for (gap = n / 2; gap > 0; gap /= 2) //步长
        for (i = 0; i < gap; i++)        //直接插入排序
        {
            for (j = i + gap; j < n; j += gap) 
                if (a[j] < a[j - gap])
                {
                    int temp = a[j];
                    int k = j - gap;
                    while (k >= 0 && a[k] > temp)
                    {
                        a[k + gap] = a[k];
                        k -= gap;
                    }
                    a[k + gap] = temp;
                }
        }
}
/*改进和优化,以第二次排序为例,原来是每次从1A到1E,从2A到2E,可以改成从1B开始,先和1A比较,然后取2B与2A比较,再取1C与前面自己组内的数据比较…….。这种每次从数组第gap个元素开始,每个元素与自己组内的数据进行直接插入排序显然也是正确的。*/
void shellsort2(int a[], int n)
{
    int j, gap;

    for (gap = n / 2; gap > 0; gap /= 2)
        for (j = gap; j < n; j++)//从数组第gap个元素开始
            if (a[j] < a[j - gap])//每个元素与自己组内的数据进行直接插入排序
            {
                int temp = a[j];
                int k = j - gap;
                while (k >= 0 && a[k] > temp)
                {
                    a[k + gap] = a[k];
                    k -= gap;
                }
                a[k + gap] = temp;
            }
}
//冒泡思想
void shellsort3(int a[], int n)
{
    int i, j, gap;

    for (gap = n / 2; gap > 0; gap /= 2)
        for (i = gap; i < n; i++)
            for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)
                Swap(a[j], a[j + gap]);
}

699个结点的完全二叉树,有叶子节点多少个?

完全二叉树(Complete Binary Tree) 
若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的节点都连续集中在最左边,这就是完全二叉树. 完全二叉树是由满二叉树而引出来的.对于深度为K的,有N个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树.
做这种题目你要知道二叉树的两个特点!第k层的节点个数最多2^(k-1)个,高度为k层的二叉树,最多2^k-1个节点!
则在本题目敏感词699个节点,因为是完全二叉树,2^10-1>699>2^9-1,所以高度为10,可以确定1到9层全满,节点总算为511,剩下的188个肯定为叶子节点!第10层上的188个节点挂在第九层的188/2=94个节点上,则第九层剩下的2^(9-1)-94=162个也为叶子节点,最后总共188+162=350个叶子节点!

大鱼吃小鱼

参考:https://blog.csdn.net/LYJ_viviani/article/details/70163133

有N条鱼每条鱼的位置及大小均不同,他们沿着X轴游动,有的向左,有的向右。游动的速度是一样的,两条鱼相遇大鱼会吃掉小鱼。从左到右给出每条鱼的大小和游动的方向(0表示向左,1表示向右)。问足够长的时间之后,能剩下多少条鱼?

#include <bits/stdc++.h>
using namespace std;  

int main()  
{  
    stack<int> s;  
    int n,a,b,num;  
    cin>>n;  
    num=n;  
    while(n--)  
    {  
        cin>>a>>b;  
        if(b==1) 
            s.push(a);  
        else  
        {  
            while(!s.empty())  
            {  
                if(a>s.top()) 
                {
                    s.pop();
                    num--;

                }  
                else 
                { 
                    num--;
                    break;
                }  
            }  
        }  
    }  
    cout<<num<<endl;  
    return 0;  
}  

判断一个链表中是否有环

快慢指针中的快慢指的是移动的步长,即每次向前移动速度的快慢。例如可以让快指针每次沿链表向前移动2,慢指针每次向前移动1次。如果链表存在环,就好像操场的跑道是一个环形一样。此时让快慢指针都从链表头开始遍历,快指针每次向前移动两个位置,慢指针每次向前移动一个位置;如果快指针到达NULL,说明链表以NULL为结尾,没有环。如果快指针追上慢指针,则表示有环。代码如下:

bool HasCircle(Node *head)
{
  if(head == NULL)
    return false;
  Node *slow = head, *fast = head;
  while(fast != NULL && fast->next!=NULL)
  {
     slow = slow->next; //慢指针每次前进一步
     fast = fast->next->next;//快指针每次前进两步
     if(slow == fast) //相遇,存在环
         return true;
  }
  return false;
}

链表反转

参考:https://blog.csdn.net/blioo/article/details/62050967

总结一下单链表的反转:

  • 保存当前头结点的下个节点。
  • 将当前头结点的下一个节点指向“上一个节点”,这一步是实现了反转。
  • 将当前头结点设置为“上一个节点”。
  • 将保存的下一个节点设置为头结点。
linkList reverse(linkList head){
  linkList p,q,pr;
  p = head->next;
  q = NULL;
  head->next = NULL;
  while(p){
    pr = p->next;
    p->next = q;
    q = p;
    p = pr;
  }
  head->next = q;
  return head;
}

判断括号是否有效

参考:https://www.cnblogs.com/maoqifansBlog/p/12498130.html

#include <iostream>
#include <stack>
#include <string>
#include <map>
using namespace std;

class Solution
{
public:
    bool isValid(string s)
    {
        if (s == "") // 如果时空字符串也合法
            return true;
        if (s.size() == 1) // 只有一个字符肯定非法
            return false;
        stack<char> st;
        map<char, char> mp = {{')', '('}, {'}', '{'}, {']', '['}}; // 映射括号
        for (int i = 0; i < s.size(); i++)
        {
            if (mp.find(s[i]) != mp.end()) // 查找mp是否映射了该符号
            {
                char top_ele = (st.size() == 0) ? '#' : st.top(); // 获取栈顶元素,若为空则随便设置一个字符
                if (top_ele != '#')                               // 栈不为空则弹出元素
                    st.pop();
                if (top_ele != mp.find(s[i])->second) // 如果这个元素被弹出的元素和mp对应映射的值不一样,则直接返回false
                    return false;
            }
            else
            {
                st.push(s[i]); //压入栈中
            }
        }
        return st.size() == 0; // 如果栈为空则表示合法
    }
};

C++基础

externC

参考:https://www.jianshu.com/p/5d2eeeb93590

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

标准头文件如下:

#ifndef __INCvxWorksh  /*防止该头文件被重复引用*/
#define __INCvxWorksh

#ifdef __cplusplus    //__cplusplus是cpp中自定义的一个宏
extern "C" {          //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
#endif

    /**** some declaration or so *****/  

#ifdef __cplusplus
}
#endif

#endif /* __INCvxWorksh */

extern "C"的含义

extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
被extern "C"限定的函数或变量是extern类型的;
1、extern关键字
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在链接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

2、被extern "C"修饰的变量和函数是按照C语言方式编译和链接的
首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。
** _foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。** 例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

3、举例说明
(1)未加extern "C"声明时的连接方式
假设在C++中,模块A的头文件如下:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
//在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);

实际上,在连接阶段,链接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

(2)加extern "C"声明后的编译和链接方式
加extern "C"声明后,模块A的头文件变为:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif

在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
<1>A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
<2>链接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo(int x, int y),则模块B找不到模块A中的函数;反之亦然。extern “C”这个声明的真实目的是为了实现C++与C及其它语言的混合编程

应用场合

  • C++代码调用C语言代码、在C++的头文件中使用
    在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);     //注:写成extern "C" int add(int , int ); 也可以
#endif

/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
 return x + y;
}

// c++实现文件,调用add:cppFile.cpp
extern "C"
{
 #include "cExample.h"        //注:此处不妥,如果这样编译通不过,换成 extern "C" int add(int , int ); 可以通过
}

int main(int argc, char* argv[])
{
 add(2,3);
 return 0;
}

如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C"{}。

  • 在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif

//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
 return x + y;
}

/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
 add( 2, 3 );
 return 0;
}

虚函数使用范围及不同情况

参考:https://blog.csdn.net/wanghuaide/article/details/5972559?depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-1&utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-1

1、什么是虚函数

C++书中介绍为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。传统的多态实际上就是由虚函数(Virtual Function)利用虚表(Virtual Table)实现的也就是说,虚函数应为多态而生。

2、什么时候用到虚函数

既然虚函数虚函数应为多态而生,那么简单的说当我们在C++和C#中要想实现多态的方法之一就是使用到虚函数。复杂点说,那就是因为OOP的核心思想就是用程序语言描述客观世界的对象,从而抽象出一个高内聚、低偶合,易于维护和扩展的模型。

但是在抽象过程中我们会发现很多事物的特征不清楚,或者很容易发生变动,怎么办呢?比如飞禽都有飞这个动作,但是对于不同的鸟类它的飞的动作方式是 不同的,有的是滑行,有的要颤抖翅膀,虽然都是飞的行为,但具体实现却是千差万别,在我们抽象的模型中不可能把一个个飞的动作都考虑到,那么怎样为以后留 下好的扩展,怎样来处理各个具体飞禽类千差万别的飞行动作呢?比如我现在又要实现一个类“鹤”,它也有飞禽的特征(比如飞这个行为),如何使我可以只用简 单地继承“飞禽”,而不去修改“飞禽”这个抽象模型现有的代码,从而达到方便地扩展系统呢?

因此面向对象的概念中引入了虚函数来解决这类问题。

使用虚函数就是在父类中把子类中共有的但却易于变化或者不清楚的特征抽取出来,作为子类需要去重新实现的操作(override)。而虚函数也是OOP中实现多态的关键之一。

3、虚函数/接口/抽象函数

虚函数:可由子类继承并重写的函数,后期绑定
抽像函数:规定其非虚子类必须实现的函数,必须被重写。
接口:必须重写

多态种类和区别

C++ 中的多态性具体体现在编译和运行两个阶段。编译时多态是静态多态,在编译时就可以确定使用的接口。运行时多态是动态多态,具体引用的接口在运行时才能确定。

 

静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的。静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早绑定。静态多态往往也被叫做静态联编。 动态多态则是指函数调用的地址不能在编译器期间确定,需要在运行时确定,属于晚绑定,动态多态往往也被叫做动态联编。
多态的作用:为了接口重用。静态多态,将同一个接口进行不同的实现,根据传入不同的参数(个数或类型不同)调用不同的实现。动态多态,则不论传递过来的哪个类的对象,函数都能够通过同一个接口调用到各自对象实现的方法。

静态多态和动态多态的区别

静态多态往往通过函数重载和模版(泛型编程)来实现。

动态多态最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而调用不同的方法。如果没有使用虚函数,即没有利用 C++ 多态性,则利用基类指针调用相应函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用同一个函数,这就无法达到“一个接口,多种实现”的目的了。

C++内存分配方式,静态变量在哪儿

,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

,就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。

自由存储区,就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

静态变量存在全区/静态存储区。

C++中的static关键字

参考:https://www.cnblogs.com/beyondanytime/archive/2012/06/08/2542315.html

分为面向过程和面向对象两种类型

面向过程中的static

静态全局/局部变量/静态函数:静态变量都在全局数据区分配内存,包括后面将要提到的静态局部变量。

静态全局变量有以下特点:
• 该变量在全局数据区分配内存;
• 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
• 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的; 

静态局部变量有以下特点
• 该变量在全局数据区分配内存;
• 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
• 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
• 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

静态函数

在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。

定义静态函数的好处:
• 静态函数不能被其它文件所用;
• 其它文件中可以定义相同名字的函数,不会发生冲突;

面向对象中的static

静态数据成员
在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。

#include <iostream.h>
class Myclass
{
public:
   Myclass(int a,int b,int c);
   void GetSum();
private:
   int a,b,c;
   static int Sum;//声明静态数据成员
};
int Myclass::Sum=0;//定义并初始化静态数据成员

Myclass::Myclass(int a,int b,int c)
{
   this->a=a;
   this->b=b;
   this->c=c;
   Sum+=a+b+c;
}

void Myclass::GetSum()
{
   cout<<"Sum="<<Sum<<endl;
}

void main()
{
   Myclass M(1,2,3);
   M.GetSum();
   Myclass N(4,5,6);
   N.GetSum();
   M.GetSum();

}

静态数据成员有以下特点:
• 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
• 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。在Example 5中,语句int Myclass::Sum=0;是定义静态数据成员;
• 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
• 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
• 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
<数据类型><类名>::<静态数据成员名>=<值>
• 类的静态数据成员有两种访问形式:
<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;
• 静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;
• 同全局变量相比,使用静态数据成员有两个优势:
1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;

静态成员函数
与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。

class Myclass
{
public:
   Myclass(int a,int b,int c);
   static void GetSum();/声明静态成员函数
private:
   int a,b,c;
   static int Sum;//声明静态数据成员
};
int Myclass::Sum=0;//定义并初始化静态数据成员

Myclass::Myclass(int a,int b,int c)
{
   this->a=a;
   this->b=b;
   this->c=c;
   Sum+=a+b+c; //非静态成员函数可以访问静态数据成员
}

void Myclass::GetSum() //静态成员函数的实现
{
  // cout<<a<<endl; //错误代码,a是非静态数据成员
   cout<<"Sum="<<Sum<<endl;
}

void main()
{
   Myclass M(1,2,3);
   M.GetSum();
   Myclass N(4,5,6);
   N.GetSum();
   Myclass::GetSum();
}

关于静态成员函数,可以总结为以下几点:
• 出现在类体外的函数定义不能指定关键字static;
• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
• 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
• 静态成员函数不能访问非静态成员函数和非静态数据成员;
• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
• 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
<类名>::<静态成员函数名>(<参数表>)
调用类的静态成员函数。

指针引用

参考:https://www.cnblogs.com/li-peng/p/4116349.html

在C和C++中,指针一般指的是某块内存的地址,通过这个地址,我们可以寻址到这块内存;而引用是一个变量的别名,例如我们给小明起了个外号:明明,那我们说明明的时候,就是说小明。

智能指针

参考:https://www.cnblogs.com/TenosDoIt/p/3456704.html

C++11:https://www.cnblogs.com/wxquare/p/4759020.html

 C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

理解智能指针需要从下面三个层次:

  1. 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  2. 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
  3. 智能指针还有一个作用是把值语义转换成引用语义。

智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr

auto_ptr

class Test
{
public:
    Test(string s)
    {
        str = s;
       cout<<"Test creat\n";
    }
    ~Test()
    {
        cout<<"Test delete:"<<str<<endl;
    }
    string& getStr()
    {
        return str;
    }
    void setStr(string s)
    {
        str = s;
    }
    void print()
    {
        cout<<str<<endl;
    }
private:
    string str;
};
 
 
int main()
{
    auto_ptr<Test> ptest(new Test("123"));
    ptest->setStr("hello ");
    ptest->print();
    ptest.get()->print();
    ptest->getStr() += "world !";
    (*ptest).print();
    ptest.reset(new Test("123"));
    ptest->print();
    return 0;
}

如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针auto_ptr<Test>ptest();

当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL)。

unique_ptr

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

1、拥有它指向的对象

2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作

3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr 可以实现如下功能:

1、为动态申请的内存提供异常安全

2、讲动态申请的内存所有权传递给某函数

3、从某个函数返回动态申请内存的所有权

4、在容器中保存指针

5、auto_ptr 应该具有的功能

unique_ptr<Test> fun()
{
    return unique_ptr<Test>(new Test("789"));
}
int main()
{
    unique_ptr<Test> ptest(new Test("123"));
    unique_ptr<Test> ptest2(new Test("456"));
    ptest->print();
    ptest2 = std::move(ptest);//不能直接ptest2 = ptest
    if(ptest == NULL)cout<<"ptest = NULL\n";
    Test* p = ptest2.release();
    p->print();
    ptest.reset(p);
    ptest->print();
    ptest2 = fun(); //这里可以用=,因为使用了移动构造函数
    ptest2->print();
    return 0;
}

 

share_ptr

从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

int main()
{
    shared_ptr<Test> ptest(new Test("123"));
    shared_ptr<Test> ptest2(new Test("456"));
    cout<<ptest2->getStr()<<endl;
    cout<<ptest2.use_count()<<endl;
    ptest = ptest2;//"456"引用次数加1,“123”销毁
    ptest->print();
    cout<<ptest2.use_count()<<endl;//2
    cout<<ptest.use_count()<<endl;//2
    ptest.reset();
    ptest2.reset();//此时“456”销毁
    cout<<"done !\n";
    return 0;
}

weak_ptr

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
    {
        cout<<"A delete\n";
    }
};
class B
{
public:
    shared_ptr<A> pa_;
    ~B()
    {
        cout<<"B delete\n";
    }
};
 
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
 
int main()
{
    fun();
    return 0;
}

迭代器使用(map set vector list)

答:vector,支持下标,内存连续,resize,capacity,还分析push_back

vector特点是:其容量在需要时可以自动分配,本质上是数组形式的存储方式。即在索引可以在常数时间内完成。缺点是在插入或者删除一项时,需要线性时间。但是在尾部插入或者删除,是常数时间的。

list 是双向链表:如果知道位置,在其中进行插入和删除操作时,是常数时间的。索引则需要线性时间(和单链表一样)。

vector 和 list 都支持在常量的时间内在容器的末尾添加或者删除项,vector和list都支持在常量的时间内访问表的前端的项.

vector会一次分配多个元素内存,那么下次增加时,只是改写内存而已,就不会再分配内存了,但是list每次只分配一个元素的内存,每次增加一个元素时,都会执行一次内存分配行为。如果是大量数据追加,建议使用list,因为vector 在有大量元素,并且内存已满,再pushback元素时,需要分配大块内存并把已经有的数据move到新分配的内存中去(vector不能加入不可复制元素)然后再释放原来的内存块,这些操作很耗时。而list的性能而会始终于一的,不会出现vector的性能变化情况,所以对于容器构件,需要用什么类型最好,取决于业务逻辑。

Map也是一种关联容器,它是 键—值对的集合,即它的存储都是以一对键和值进行存储的,Map通常也可以理解为关联数组(associative array),就是每一个值都有一个键与之一一对应,因此,map也是不允许重复元素出现的。

底层数据结构是红黑树。具体理解参考:https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md

插入数据几种方式

方法一:pair

例:

map<int, string> mp;

mp.insert(pair<int,string>(1,"aaaaa"));

方法二:make_pair

例:

map<int, string> mp;

mp.insert(make_pair<int,string>(2,"bbbbb"));

方法三:value_type

例:

map<int, string> mp;

mp.insert(map<int, string>::value_type(3,"ccccc"));

方法四:[]

例:

map<int, string> mp;

mp[4] = "ddddd";

四种方法异同:前三种方法当出现重复键时,编译器会报错,而第四种方法,当键重复时,会覆盖掉之前的键值对。


vector与list元素个数相同,遍历一遍,哪个快

vector更快,list需要指针,vector是连续内存,因为有L1L2L3缓存,可以更快,缓存层级,金字塔,经常使用的话放在更上一层缓存,读取就更快 .

红黑树和平衡二叉树区别

AVL是严格平衡,旋转可能更多,红黑树的优势在于稳定性,插入和删除都是O(logn)

哈希表

参考:https://blog.csdn.net/weixin_38169413/article/details/81612307

概念:根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

构造方法:直接定址法、除留取余法、平方取中法、折叠法等。

解决冲突方法

  • 换个位置: 开放地址法
  • 同一位置的冲突对象组织在一起: 链地址法

STL的allocator

参考:https://zhuanlan.zhihu.com/p/34725232

allocator是STL的重要组成,但是一般用户不怎么熟悉他,因为allocator隐藏在所有容器(包括vector)身后,默默完成内存配置与释放,对象构造和析构的工作

new和malloc的内存对齐

大多数情况下,编译器和C库透明地帮你处理对齐问题。POSIX 标明了通过malloc( ), calloc( ), 和 realloc( ) 返回的地址对于任何的C类型来说都是对齐的。

  对齐参数(MALLOC_ALIGNMENT) 大小的设定并需满足两个特性

 1.必须是2的幂

 2.必须是(void *)的整数倍

  至于为什么会要求是(void *)的整数倍,这个目前我还不太清楚,等你来发现...

  根据这个原理,在32位和64位的对齐单位分别为8字节和16字节

  但是这并解释不了上面的测试结果,这是因为系统malloc分配的最小单位(MINSIZE)并不是对齐单位。

在32位系统中MINSIZE为16字节,在64位系统中MINSIZE一般为32字节。从request2size还可以知道,如果是64位系统,申请内存为1~24字节时,系统内存消耗32字节,当申请内存为25字节时,系统内存消耗48字节。 如果是32位系统,申请内存为1~12字节时,系统内存消耗16字节,当申请内存为13字节时,系统内存消耗24字节。 

特化,偏特化

参考:https://www.jianshu.com/p/4be97bf7a3b9

有时为了需要,针对特定的类型,需要对模板进行特化,也就是所谓的特殊处理。

那模板的偏特化呢?所谓的偏特化是指提供另一份template定义式,而其本身仍为templatized;也就是说,针对template参数更进一步的条件限制所设计出来的一个特化版本。这种偏特化的应用在STL中是随处可见的。与模板特化的区别在于,模板特化以后,实际上其本身已经不是templatized,而偏特化,仍然带有templatized。

虚函数存在哪个内存区

1.虚函数表是全局共享的元素,即全局仅有一个.

2.虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表.即虚函数表不是函数,不是程序代码,不肯能存储在代码段.

3.虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆中.

根据以上特征,虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,大小确定.

所以我推测虚函数表和静态成员变量一样,存放在全局数据区.

c/c++程序所占用的内存一共分为五种:

栈区,堆区,程序代码区,全局数据区(静态区),文字常量区.

显而易见,虚函数表存放在全局数据区.

几个值得注意的问题

  1.   虚函数表是class specific的,也就是针对一个类来说的,这里有点像一个类里面的staic成员变量,即它是属于一个类所有对象的,不是属于某一个对象特有的,是一个类所有对象共有的。
  2.  虚函数表是编译器来选择实现的,编译器的种类不同,可能实现方式不一样,就像前面我们说的vptr在一个对象的最前面,但是也有其他实现方式,不过目前gcc 和微软的编译器都是将vptr放在对象内存布局的最前面。
  3.  虽然我们知道vptr指向虚函数表,那么虚函数表具体存放在内存哪个位置呢,虽然这里我们已经可以得到虚函数表的地址。实际上虚函数指针是在构造函数执行时初始化的,而虚函数表是存放在可执行文件中的。下面的一篇博客测试了微软的编译器将虚函数表存放在了目标文件或者可执行文件的常量段中,http://blog.csdn.net/vicness/article/details/3962767,不过我在gcc下的汇编文件中没有找到vtbl的具体存放位置,主要是对可执行文件的装载和运行原理还没有深刻的理解,相信不久有了这些知识之后会很轻松的找到虚函数表到底存放在目标文件的哪一个段中。
  4. 经过测试,在gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。

虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别。

数据库

数据库面试题汇总参考:https://blog.csdn.net/qq_27262727/article/details/104875370

mysql底层索引及为什么用这个索引

参考:https://juejin.im/post/5c822b0ce51d453a42155c3d

索引概念:索引就像书的目录Mysql官网是这么定义的:Indexes are used to find rows with specific column values quickly我是这么定义的:索引是一种优化查询的数据结构

为什么哈希表、完全平衡二叉树、B树、B+树都可以优化查询,为何Mysql独独喜欢B+树?

哈希表

哈希表(Hash table,也叫散列表),是根据键值(Key value)而直接进行访问的数据结构。也就是说,它通过把键值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。哈希表的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。

问题:容易出现哈希冲突,哈希表的特点就是可以快速的精确查询,但是不支持范围查询

完全平衡二叉树

图中的每一个节点实际上应该有四部分:

  1. 左指针,指向左子树
  2. 键值
  3. 键值所对应的数据的存储地址
  4. 右指针,指向右子树

另外需要提醒的是,二叉树是有顺序的,简单的说就是“左边的小于右边的”假如我们现在来查找‘周瑜’,需要找2次(第一次曹操,第二次周瑜),比哈希表要多一次。而且由于完全平衡二叉树是有序的,所以也是支持范围查找的。

B树

还是上面的表数据用B树表示如下图(为了简单,数据对应的地址就不画在图中了。):

我们可以发现同样的元素,B树的表示要比完全平衡二叉树要“矮”,原因在于B树中的一个节点可以存储多个元素。

B+树

还是上面的表数据用B+树表示如下图(为了简单,数据对应的地址就不画在图中了。):

我们可以发现同样的元素,B+树的表示要比B树要“胖”,原因在于B+树中的非叶子节点会冗余一份在叶子节点中,并且叶子节点之间用指针相连。

B+树到底有什么优势呢?

这里我们用“反证法”,假如我们现在就用完全平衡二叉树作为索引的数据结构,我们来看一下有什么不妥的地方。实际上,索引也是很“大”的,因为索引也是存储元素的,我们的一个表的数据行数越多,那么对应的索引文件其实也是会很大的,实际上也是需要存储在磁盘中的,而不能全部都放在内存中,所以我们在考虑选用哪种数据结构时,我们可以换一个角度思考,哪个数据结构更适合从磁盘中读取数据,或者哪个数据结构能够提高磁盘的IO效率。回头看一下完全平衡二叉树,当我们需要查询“张飞”时,需要以下步骤

  1. 从磁盘中取出“曹操”到内存,CPU从内存取出数据进行笔记,“张飞”<“曹操”,取左子树(产生了一次磁盘IO)
  2. 从磁盘中取出“周瑜”到内存,CPU从内存取出数据进行笔记,“张飞”>“周瑜”,取右子树(产生了一次磁盘IO)
  3. 从磁盘中取出“孙权”到内存,CPU从内存取出数据进行笔记,“张飞”>“孙权”,取右子树(产生了一次磁盘IO)
  4. 从磁盘中取出“黄忠”到内存,CPU从内存取出数据进行笔记,“张飞”=“张飞”,找到结果(产生了一次磁盘IO)

同理,回头看一下B树,我们发现只发送三次磁盘IO就可以找到“张飞”了,这就是B树的优点:一个节点可以存储多个元素,相对于完全平衡二叉树所以整棵树的高度就降低了,磁盘IO效率提高了。而,B+树是B树的升级版,只是把非叶子节点冗余一下,这么做的好处是为了提高范围查找的效率

所以,到这里,我们可以总结出来,Mysql选用B+树这种数据结构作为索引,可以提高查询索引时的磁盘IO效率,并且可以提高范围查询的效率,并且B+树里的元素也是有序的。

mysql 事务ACID

参考:https://www.cnblogs.com/kismetv/p/10331633.html

mysql事务

概念:事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL支持事务。

MySQL服务器逻辑架构从上往下可以分为三层:

(1)第一层:处理客户端连接、授权认证等。

(2)第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。

(3)第三层:存储引擎,负责MySQL中数据的存储和提取。MySQL中服务器层不管理事务,事务是由存储引擎实现的。MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。

ACID特性

ACID是衡量事务的四个特性:

  • 原子性(Atomicity,或称不可分割性):一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
  • 一致性(Consistency):事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
  • 隔离性(Isolation):事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
  • 持久性(Durability):事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

按照严格的标准,只有同时满足ACID特性才是事务;但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少。例如MySQL的NDB Cluster事务不满足持久性和隔离性;InnoDB默认事务隔离级别是可重复读,不满足隔离性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性……因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。

mysql隔离级别及实现

参考:https://www.jianshu.com/p/dab1c0ecbac0

有四种事务隔离级别:

Read uncommitted 读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
Read committed 读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
Repeatable read 重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
Serializable 串行读,是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

查询是否使用索引(explain)

参考:https://blog.csdn.net/vtopqx/article/details/86504206

mysql> explain  SELECT * FROM tb_user
 WHERE STATUS=1 limit 0,20;
+----+-------------+----------------+------------+------+----------------------+----------------------+---------+-------+-------+----------+-------+
| id | select_type | table          | partitions | type | possible_keys        | key                  | key_len | ref   | rows  | filtered | Extra |
+----+-------------+----------------+------------+------+----------------------+----------------------+---------+-------+-------+----------+-------+
|  1 | SIMPLE      | tb_news_online | NULL       | ref  | idx_tb_news_online_9 | idx_tb_news_online_9 | 5       | const | 99494 |      100 | NULL  |
+----+-------------+----------------+------------+------+----------------------+----------------------+---------+-------+-------+----------+-------+
1 row in set
 
mysql> 

EXPLAIN列的解释:

table:显示这一行的数据是关于哪张表的

type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、index和ALL
type显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般来说,得保证查询至少达到range级别,最好能达到ref。

possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句

key: 实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MYSQL会选择优化不足的索引。这种情况下,可以在SELECT语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MYSQL忽略索引

key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好

ref:显示索引的哪一列被使用了,如果可能的话,是一个常数

rows:MYSQL认为必须检查的用来返回请求数据的行数

Extra:关于MYSQL如何解析查询的额外信息。将在表4.3中讨论,但这里可以看到的坏的例子是Using temporary和Using filesort,意思MYSQL根本不能使用索引,结果是检索会很慢

sql优化

参考:

https://database.51cto.com/art/200904/118526.htm

https://www.cnblogs.com/yunfeifei/p/3850440.html

  • 查询语句无论是使用哪种判断条件 等于、小于、大于, WHERE 左侧的条件查询字段不要使用函数或者表达式

  • 使用 EXPLAIN 命令优化你的 SELECT 查询,对于复杂、效率低的 sql 语句,我们通常是使用 explain sql 来分析这条 sql 语句,这样方便我们分析,进行优化。

  • 当你的 SELECT 查询语句只需要使用一条记录时,要使用 LIMIT 1

  • 不要直接使用 SELECT *,而应该使用具体需要查询的表字段,因为使用 EXPLAIN 进行分析时,SELECT * 使用的是全表扫描,也就是 type = all

  • 为每一张表设置一个 ID 属性

  • 避免在 WHERE 字句中对字段进行 NULL 判断

  • 避免在 WHERE 中使用 != 或 <> 操作符

  • 使用 BETWEEN AND 替代 IN

  • 为搜索字段创建索引

  • 选择正确的存储引擎,InnoDB 、MyISAM 、MEMORY 等

  • 使用 LIKE %abc% 不会走索引,而使用 LIKE abc% 会走索引

  • 对于枚举类型的字段(即有固定罗列值的字段),建议使用ENUM而不是VARCHAR,如性别、星期、类型、类别等

  • 拆分大的 DELETE 或 INSERT 语句

  • 选择合适的字段类型,选择标准是 尽可能小、尽可能定长、尽可能使用整数

  • 字段设计尽可能使用 NOT NULL

  • 进行水平切割或者垂直分割

Mysql MyISAM和InnoDB的区别

参考:https://blog.csdn.net/qq_27262727/article/details/104875370

关于二者的对比与总结:

count运算上的区别:因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存。
是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
是否支持外键: MyISAM不支持,而InnoDB支持。
MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作为主库的存储引擎。 一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM是最好的选择。

MyISAM 存储引擎的特点

在 5.1 版本之前,MyISAM 是 MySQL 的默认存储引擎,MyISAM 并发性比较差,使用的场景比较少,主要特点是

  • 不支持事务操作,ACID 的特性也就不存在了,这一设计是为了性能和效率考虑的。

  • 不支持外键操作,如果强行增加外键,MySQL 不会报错,只不过外键不起作用。

  • MyISAM 默认的锁粒度是表级锁,所以并发性能比较差,加锁比较快,锁冲突比较少,不太容易发生死锁的情况。

  • MyISAM 会在磁盘上存储三个文件,文件名和表名相同,扩展名分别是 .frm(存储表定义).MYD(MYData,存储数据)MYI(MyIndex,存储索引)。这里需要特别注意的是 MyISAM 只缓存索引文件,并不缓存数据文件。

  • MyISAM 支持的索引类型有 全局索引(Full-Text)B-Tree 索引R-Tree 索引

    Full-Text 索引:它的出现是为了解决针对文本的模糊查询效率较低的问题。

    B-Tree 索引:所有的索引节点都按照平衡树的数据结构来存储,所有的索引数据节点都在叶节点

    R-Tree索引:它的存储方式和 B-Tree 索引有一些区别,主要设计用于存储空间和多维数据的字段做索引,目前的 MySQL 版本仅支持 geometry 类型的字段作索引,相对于 BTREE,RTREE 的优势在于范围查找。

  • 数据库所在主机如果宕机,MyISAM 的数据文件容易损坏,而且难以恢复。

  • 增删改查性能方面:SELECT 性能较高,适用于查询较多的情况

InnoDB 存储引擎的特点

自从 MySQL 5.1 之后,默认的存储引擎变成了 InnoDB 存储引擎,相对于 MyISAM,InnoDB 存储引擎有了较大的改变,它的主要特点是

  • 支持事务操作,具有事务 ACID 隔离特性,默认的隔离级别是可重复读(repetable-read)、通过MVCC(并发版本控制)来实现的。能够解决脏读不可重复读的问题。

  • InnoDB 支持外键操作。

  • InnoDB 默认的锁粒度行级锁,并发性能比较好,会发生死锁的情况。

  • 和 MyISAM 一样的是,InnoDB 存储引擎也有 .frm文件存储表结构 定义,但是不同的是,InnoDB 的表数据与索引数据是存储在一起的,都位于 B+ 数的叶子节点上,而 MyISAM 的表数据和索引数据是分开的。

  • InnoDB 有安全的日志文件,这个日志文件用于恢复因数据库崩溃或其他情况导致的数据丢失问题,保证数据的一致性。

  • InnoDB 和 MyISAM 支持的索引类型相同,但具体实现因为文件结构的不同有很大差异。

  • 增删改查性能方面,如果执行大量的增删改操作,推荐使用 InnoDB 存储引擎,它在删除操作时是对行删除,不会重建表。

MyISAM 和 InnoDB 存储引擎的对比

  • 锁粒度方面:由于锁粒度不同,InnoDB 比 MyISAM 支持更高的并发;InnoDB 的锁粒度为行锁、MyISAM 的锁粒度为表锁、行锁需要对每一行进行加锁,所以锁的开销更大,但是能解决脏读和不可重复读的问题,相对来说也更容易发生死锁

  • 可恢复性上:由于 InnoDB 是有事务日志的,所以在产生由于数据库崩溃等条件后,可以根据日志文件进行恢复。而 MyISAM 则没有事务日志。

  • 查询性能上:MyISAM 要优于 InnoDB,因为 InnoDB 在查询过程中,是需要维护数据缓存,而且查询过程是先定位到行所在的数据块,然后在从数据块中定位到要查找的行;而 MyISAM 可以直接定位到数据所在的内存地址,可以直接找到数据。

  • 表结构文件上:MyISAM 的表结构文件包括:.frm(表结构定义),.MYI(索引),.MYD(数据);而 InnoDB 的表数据文件为:.ibd和.frm(表结构定义);

主键索引和非主键索引的区别

参考:https://www.jianshu.com/p/f3a1e17a4df6

主键索引和非主键索引的区别是:非主键索引的叶子节点存放的是主键的值,而主键索引的叶子节点存放的是整行数据。非主键索引也被称为二级索引,而主键索引也被称为聚簇索引

主键是逻辑键,索引是物理键,意思就是主键不实际存在,而索引实际存在在数据库中
索引会真正的产生一个文件的
数据会真正的产生一个文件的
redo log 记录的是物理日志"某个数据页上做了什么修改"  循环使用
bin log 记录的是逻辑日志 语句的原始逻辑"ID=1 ,2 " 追加使用

最左前缀

在mysql建立联合索引时会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配,示例:
对列col1、列col2和列col3建一个联合索引

KEY test_col1_col2_col3 on test(col1,col2,col3);

联合索引 test_col1_col2_col3 实际建立了(col1)、(col1,col2)、(col,col2,col3)三个索引。

SELECT * FROM test WHERE col1=“1” AND clo2=“2” AND clo4=“4”

上面这个查询语句执行时会依照最左前缀匹配原则,检索时会使用索引(col1,col2)进行数据匹配。

注意:索引的字段可以是任意顺序的。

MYSQL MVCC

MVCC是一种多版本并发控制机制,大多数的MYSQL事务型存储引擎,如,InnoDB,Falcon以及PBXT都不使用一种简单的行锁机制.事实上,他们都和MVCC–多版本并发控制来一起使用.大家都应该知道,锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。

MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制.

什么是脏读?幻读?不可重复读?Innodb怎么解决幻读?

脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
RR 下,innodb下的幻读是由MVCC 或者 GAP 锁 或者是next-key lock 解决的。

MVCC判断了记录的可见性,比如 select count(*) from table where col_name = xxx 时(属于快照读),在RR 级别下,这条事务在事务一开始就生成了readview,通过这个readview 这条语句将会找到符合条件的行并且计算数量。 那么关于与如何找到这些符合条件的行,满足where 条件的同时也得满。

参考:幻读问题是指一个事务的两次不同时间的相同查询返回了不同的的结果集。例如:一个 select 语句执行了两次,但是在第二次返回了第一次没有返回的行,那么这些行就是“phantom” row.read view(或者说 MVCC)实现了一致性不锁定读(Consistent Nonlocking Reads),从而避免了(非当前读下)幻读
 

计算机网络

https和http

区别:HTTP + 加密 + 认证 + 完整性保护 = HTTPS

参考:https://www.jianshu.com/p/6c981b44293d

SSL/TLS 握手过程

参考:https://www.jianshu.com/p/7158568e4867

参考:https://imququ.com/post/optimize-tls-handshake.html

长连接短连接

参考:https://www.cnblogs.com/gotodsp/p/6366163.html

HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。

客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,

例子:

网上购物来说,HTTP协议是指的那个快递单,你寄件的时候填的单子就像是发了一个HTTP请求,等货物运到地方了,快递员会根据你发的请求把货物送给相应的收货人。而TCP协议就是中间运货的那个大货车,也可能是火车或者飞机,但不管是什么,它是负责运输的,因此必须要有路,不管是地上还是天上。那么这个路就是所谓的TCP连接,也就是一个双向的数据通道。

tcp拥塞控制

参考:https://www.cnblogs.com/wuchanming/p/4422779.html

原因:网络中的路由器会有一个数据包处理队列,当路由器接收到的数据包太多而一下子处理不过来时,就会导致数据包处理队列过长。此时,路由器就会无条件的丢弃新接收到的数据封包。

方法:慢开始与拥塞避免/快重传和快恢复

1、慢开始

发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。

慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。这里用报文段的个数的拥塞窗口大小举例说明慢开始算法,实时拥塞窗口大小是以字节为单位的。

2、快重传和快恢复

 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。快重传配合使用的还有快恢复算法,有以下两个要点:

①当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。

②考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。如下图:

UDP和TCP区别,分别怎么处理丢包

TCP与UDP基本区别

  1.基于连接与无连接
  2.TCP要求系统资源较多,UDP较少; 
  3.UDP程序结构较简单 
  4.流模式(TCP)与数据报模式(UDP); 
  5.TCP保证数据正确性,UDP可能丢包 
  6.TCP保证数据顺序,UDP不保证 
UDP应用场景:

  1.面向数据报方式
  2.网络数据大多为短消息 
  3.拥有大量Client
  4.对数据安全性无特殊要求
  5.网络负担非常重,但对响应速度要求高
具体编程时的区别
        1.socket()的参数不同 
   2.UDP Server不需要调用listen和accept 
   3.UDP收发数据用sendto/recvfrom函数 
   4.TCP:地址信息在connect/accept时确定 
   5.UDP:在sendto/recvfrom函数中每次均 需指定地址信息 
   6.UDP:shutdown函数无效
TCP编程的服务器端一般步骤是: 
  1、创建一个socket,用函数socket(); 
  2、设置socket属性,用函数setsockopt(); * 可选 
  3、绑定IP地址、端口等信息到socket上,用函数bind(); 
  4、开启监听,用函数listen(); 
  5、接收客户端上来的连接,用函数accept(); 
  6、收发数据,用函数send()和recv(),或者read()和write(); 
  7、关闭网络连接; 
  8、关闭监听; 
与之对应的UDP编程步骤要简单许多,分别如下: 
  UDP编程的服务器端一般步骤是: 
  1、创建一个socket,用函数socket(); 
  2、设置socket属性,用函数setsockopt();* 可选 
  3、绑定IP地址、端口等信息到socket上,用函数bind(); 
  4、循环接收数据,用函数recvfrom(); 
  5、关闭网络连接;


TCP/UDP分别处理丢包方法

UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。并且它是将应用程序发来的数据在收到的那一刻,立刻按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况下,UDP也无法进行流量控制等避免网络拥塞的行为。此外,传输途中如果出现了丢包,UDO也不负责重发。甚至当出现包的到达顺序乱掉时也没有纠正的功能。如果需要这些细节控制,那么不得不交给由采用UDO的应用程序去处理。换句话说,UDP将部分控制转移到应用程序去处理,自己却只提供作为传输层协议的最基本功能。UDP有点类似于用户说什么听什么的机制,但是需要用户充分考虑好上层协议类型并制作相应的应用程序。

TCP充分实现了数据传输时各种控制功能,可以进行丢包的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。TCP通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输。

TCP/UDP应用场景

#TCP应用场景
当对网络通信质量有要求时,比如:整个数据要准确无误的传递给对方,这往往对于一些要求可靠的应用,比如HTTP,HTTPS,FTP等传输文件的协议,POP,SMTP等邮件的传输协议。常见使用TCP协议的应用:
1.浏览器使用的:HHTP
2.FlashFXP:FTP
3.Outlook:POP,SMTP
4.QQ文件传输

UDP 文件传输协议
对当前网络通讯质量要求不高的时候,要求网络通讯速度尽量的快,这时就使用UDP
日常生活中常见使用UDP协议:
1.QQ语音
2.QQ视频
3.TFTP

UDP怎么做简单的可靠传输

参考:https://www.jianshu.com/p/6c73a4585eba

模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。

  • 1、添加seq/ack机制,确保数据发送到对端
  • 2、添加发送和接收缓冲区,主要是用户超时重传。
  • 3、添加超时重传机制。

详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据

七层协议/TCP/IP网络模型

webp

三次握手/四次挥手/三次握手属于七层协议里面的哪一层

参考:https://blog.csdn.net/changcsw/article/details/52103640?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-4&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-4

三次握手

原因:为了解决延迟SYN报文到达的问题。面试官觉得我没答道点上,而是引导我思考如果第二次握手的SYN+ACK报文丢失了之后,客户端会不断发送SYN报文,而由于没有第三次握手,服务器端会认为连接建立,不断发送数据。这时就产生了死锁问题。这也是个不错的角度,拓展了我的思路。

所谓的“三次握手”即对每次发送的数据量是怎样跟踪进行协商使数据段的发送和接收同步,根据所接收到的数据量而确定的数据确认数及数据发送、接收完毕后何时撤消联系,并建立虚连接。为了提供可靠的传送,TCP在发送新的数据之前,以特定的顺序将数据包的序号,并需要这些包传送给目标机之后的确认消息。TCP总是用来发送大批量的数据。当应用程序在收到数据后要做出确认时也要用到TCP。

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
      第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
      第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
      第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。

四次挥手

第一次挥手:客户端给服务器发送TCP包,用来关闭客户端到服务器的数据传送。将标志位FIN和ACK置为1,序号为X=1,确认序号为Z=1。

第二次挥手:服务器收到FIN后,发回一个ACK(标志位ACK=1),确认序号为收到的序号加1,即X=X+1=2。序号为收到的确认序号=Z。

第三次挥手:服务器关闭与客户端的连接,发送一个FIN。标志位FIN和ACK置为1,序号为Y=1,确认序号为X=2。

第四次挥手:客户端收到服务器发送的FIN之后,发回ACK确认(标志位ACK=1),确认序号为收到的序号加1,即Y+1=2。序号为收到的确认序号X=2。

三次握手属于传输层。

TCP 有哪些字段

1、ACK 是 TCP 报头的控制位之一,对数据进行确认。确认由目的端发出, 用 它来告诉发送端这个序列号之前的数据段都收到了。 比如确认号为 X,则表示 前 X-1 个数据段都收到了,只有当 ACK=1 时,确认号才有效,当 ACK=0 时,确认 号无效,这时会要求重传数据,保证数据的完整性。
2、SYN 同步序列号,TCP 建立连接时将这个位置 1。
3、FIN 发送端完成发送任务位,当 TCP 完成数据传输需要断开时,,􏰅出断开 连接的一方将这位置 1。
Socket 建立网络连接的步骤:服务器监听、客户端请求、连接确认

为什么要三次握手和四次挥手?

TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,采用全双工通信。

那为什么需要三次握手呢?请看如下的过程:

  1. A向B发起建立连接请求:A——>B;
  2. B收到A的发送信号,并且向A发送确认信息:B——>A;
  3. A收到B的确认信号,并向B发送确认信号:A——>B。

三次握手大概就是这么个过程。
通过第一次握手,B知道A能够发送数据。通过第二次握手,A知道B能发送数据。结合第一次握手和第二次握手,A知道B能接收数据。结合第三次握手,B知道A能够接收数据。

至此,完成了握手过程,A知道B能收能发,B知道A能收能发,通信连接至此建立。三次连接是保证可靠的最小握手次数,再多次握手也不能提高通信成功的概率,反而浪费资源。

那为什么需要四次挥手呢?请看如下过程:

  1. A向B发起请求,表示A没有数据要发送了:A——>B;
  2. B向A发送信号,确认A的断开请求请求:B——>A;
  3. B向A发送信号,请求断开连接,表示B没有数据要发送了:B——>A;
  4. A向B发送确认信号,同意断开:A——>B。

B收到确认信号,断开连接,而A在一段时间内没收到B的信号,表明B已经断开了,于是A也断开了连接。至此,完成挥手过程。

可能有捧油会问,为什么2、3次挥手不能合在一次挥手中?那是因为此时A虽然不再发送数据了,但是还可以接收数据,B可能还有数据要发送给A,所以两次挥手不能合并为一次。

挥手次数比握手多一次,是因为握手过程,通信只需要处理连接。而挥手过程,通信需要处理数据+连接

TIMEWAIT

参考:https://blog.csdn.net/u013616945/article/details/77510925

这里写图片描述

1. time_wait状态如何产生?
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。

2.time_wait状态产生的原因

1)为实现TCP全双工连接的可靠释放

由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。

2)为使旧的数据包在网络因过期而消失

为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。

3)总结
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。

3.time_wait状态如何避免

首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。

线程间同步的方式,进程间同步的方式和进程间通信的方式

请看我写的https://blog.csdn.net/qq_27262727/article/details/105108229#%E7%BA%BF%E7%A8%8B%E9%97%B4%E5%90%8C%E6%AD%A5%E7%9A%84%E6%96%B9%E5%BC%8F%EF%BC%8C%E8%BF%9B%E7%A8%8B%E9%97%B4%E5%90%8C%E6%AD%A5%E7%9A%84%E6%96%B9%E5%BC%8F%E5%92%8C%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E7%9A%84%E6%96%B9%E5%BC%8F

浏览器打开一个网页经历了怎样的过程

1.DNS域名解析:浏览器缓存、系统缓存、路由器、ISP的DNS服务器、根域名服务器。把域名转化成IP地址。

2.与IP地址对应的服务器建立TCP连接,经历三次握手:SYN,ACK、SYN,ACK

3.以get,post方式发送HTTP请求,get方式发送主机,用户***,connection属性,cookie等

4.获得服务器的响应,显示页面

操作系统

死锁

概念:当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

死锁怎么解决?

造成死锁必须达成的4个条件(原因):

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

方法:

  • 互斥条件 ---> 独占锁的特点之一。
  • 请求与保持条件 ---> 独占锁的特点之一,尝试获取锁时并不会释放已经持有的锁
  • 不剥夺条件 ---> 独占锁的特点之一。
  • 循环等待条件 ---> 唯一需要记忆的造成死锁的条件。

进程、线程通信

参考:https://blog.csdn.net/J080624/article/details/87454764

进程间通信方式

分为两大类型:

  • 低级通信,控制信息的通信(主要用于进程之间的同步,互斥,终止和挂起等等控制信息的传递)
  • 高级通信,大批数据信息的通信(主要用于进程间数据块数据的交换和共享,常见的高级通信有管道,消息队列,共享内存等)。

IPC的方式通常有管道(包括无名管道和命名管道FIFO)、消息队列(报文)、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

linux下的进程包含以下几个关键要素:

  • 有一段可执行程序;

  • 有专用的系统堆栈空间;

  • 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;

  • 具有独立的存储空间

线程间通信

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

① 锁机制

互斥锁、条件变量、读写锁和自旋锁。

互斥锁确保同一时间只能有一个线程访问共享资源。当锁被占用时试图对其加锁的线程都进入阻塞状态(释放CPU资源使其由运行状态进入等待状态)。当锁释放时哪个等待线程能获得该锁取决于内核的调度。

读写锁当以写模式加锁而处于写状态时任何试图加锁的线程(不论是读或写)都阻塞,当以读状态模式加锁而处于读状态时“读”线程不阻塞,“写”线程阻塞。读模式共享,写模式互斥。

条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

自旋锁上锁受阻时线程不阻塞而是在循环中轮询查看能否获得该锁,没有线程的切换因而没有切换开销,不过对CPU的霸占会导致CPU资源的浪费。 所以自旋锁适用于并行结构(多个处理器)或者适用于锁被持有时间短而不希望在线程切换产生开销的情况。

② 信号量机制(Semaphore)

包括无名线程信号量和命名线程信号量。线程的信号和进程的信号量类似,使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。

参考博文:多线程并发之Semaphore(信号量)使用详解

③ 信号机制(Signal)

类似进程间的信号处理。

④ violate全局变量-共享内存

参考:https://blog.csdn.net/J080624/article/details/85318075

例子:i++

线程进程协程

一、概念

进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

二、区别:

1、进程多与线程比较

线程是指进程内的一个执行单元,也是进程内的可调度实体。线程与进程的区别:
1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
3) 线程是处理器调度的基本单位,但进程不是
4) 二者均可并发执行

5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

2、协程多与线程进行比较

1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。

2) 线程进程都是同步机制,而协程则是异步

3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

Select,epoll,epoll两种触发方式

参考:https://segmentfault.com/a/1190000003063859

I/O 多路复用就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

Select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

epoll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll两种触发方式

epoll分为两种工作方式LT和ET。

LT(level triggered) 是默认/缺省的工作方式,同时支持 block和no_block socket。这种工作方式下,内核会通知你一个fd是否就绪,然后才可以对这个就绪的fd进行I/O操作。就算你没有任何操作,系统还是会继续提示fd已经就绪,不过这种工作方式出错会比较小,传统的select/poll就是这种工作方式的代表。

ET(edge-triggered) 是高速工作方式,仅支持no_block socket,这种工作方式下,当fd从未就绪变为就绪时,内核会通知fd已经就绪,并且内核认为你知道该fd已经就绪,不会再次通知了,除非因为某些操作导致fd就绪状态发生变化。如果一直不对这个fd进行I/O操作,导致fd变为未就绪时,内核同样不会发送更多的通知,因为only once。所以这种方式下,出错率比较高,需要增加一些检测程序。

LT可以理解为水平触发,只要有数据可以读,不管怎样都会通知。而ET为边缘触发,只有状态发生变化时才会通知,可以理解为电平变化

线程池

参考:https://www.jianshu.com/p/b8197dd2934c

调用malloc时会立即分配物理内存吗?页表中一定会对应物理页框吗?swap交换空间

参考:https://blog.csdn.net/jasonLee_lijiaqi/article/details/79611293

malloc申请的就是虚拟内存,系统为每个进程创建了一个页表。在进程逻辑地址空间中的每一页,依次在页表中有一个表项,记录了该页对应的物理块号。

Linux中Swap(即:交换分区),类似于Windows的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。

CPU高速缓存的逻辑

参考:https://www.cnblogs.com/dolphin0520/p/3749259.html

LRU:https://www.cnblogs.com/dolphin0520/p/3741519.html

最近最久未使用的意思。在操作系统的内存管理中,有一类很重要的算法就是内存页面置换算法(包括FIFO,LRU,LFU等几种常见页面置换算法)。事实上,Cache算法和内存页面置换算法的核心思想是一样的:都是在给定一个限定大小的空间的前提下,设计一个原则如何来更新和访问其中的元素。下面说一下LRU算法的核心思想,LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

  而用什么数据结构来实现LRU算法呢?可能大多数人都会想到:用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。

优化做法:利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

经常使用的数据放在更高的缓存,如果在缓冲中没找到,就用LRU这种缓存置换算法 (FIFO/LFU/LRU)

信号量与条件变量的区别

参考:https://www.cnblogs.com/charlesblc/p/6143397.html

互斥锁实现的是线程之间的互斥,条件变量实现的是线程之间的同步。

对条件变量的理解:
1、条件变量与互斥锁一样,都是一种数据;
2、条件变量的作用是描述当前资源的状态,即当前资源是否就绪。
3、条件变量是在多线程程序中用来实现“等待->唤醒”逻辑的常用方法。

对信号量的理解:
1.信号量是一种特殊的变量,它只能取自然数并且只支持两种操作。
2.具有多个整数值的信号量称为通用信号量,只取 1 和 0 两个数值的称为二元信号量,这里我只讨论二元信号量。
3.信号量的两个操作:P(passeren,传递-进入临界区)、V(vrijgeven,释放-退出临界区)
假设现有信号量sv,
P操作:如果sv的值大于0,就将其减1;如果sv的值为0,就将当前线程挂起;
V操作:如果有其他线程因为等待sv而被挂起,则将其唤醒;如果没有,就将sv加1.

new和malloc的区别/delete和free的区别

与new和malloc差不多,C中malloc和free是一对,C++中new和delete是一对,delete先析构对象再回收内存,里面会调用free

申请的内存所在位置:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。

返回类型安全性:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

内存分配失败时的返回值:new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

是否需要指定内存大小:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

是否调用构造函数/析构函数:new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。

对数组的处理:C++提供了new[]与delete[]来专门处理数组类型: malloc得手动自定数组的大小

new与malloc是否可以相互调用:operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。

是否可以被重载:opeartor new /operator delete可以被重载。而malloc/free并不允许重载。

能够直观地重新分配内存:使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充,new没有这个操作。

2、malloc需要注意什么?

使用malloc申请内存空间需要了解:1)内存分配给谁?2)分配多大的内存?3)是否还有足够内存分配?4)内存将用来存储什么格式的数据?5)分配的内存在哪里?

使用malloc函数申请内存空间注意事项:1)内存是否申请成功? if( NULL !=p )2)使用结束后,一定要释放,要求malloc和free符合一夫一妻制;3)内存释放后(使用free函数之后指针变量p本身保存的地址并没有改变),需要将p的值赋值为NULL(拴住野指针)。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值