c++面试

识记

0.三大特性

封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,增加安全性

继承:是面向对象最显著的一个特性,继承是从已有的类中派生出新的类称为子类,子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。

多态

多态简单的说就是“一个函数,多种实现”,或是“一个接口,多种方法”。多态性表现在程序运行时根据传入的对象调用不同的函数。

1.内存分布

(运行前:代码区、全局区)
(运行后:栈区、堆区)

1.代码区:存放机器指令(二进制代码);特点:共享(频繁执行)、只读(防止意外修改)

2.全局区:存放全局变量、static静态变量常量(字符串常量、const全局常量)

​ 数据在程序结束后由操作系统释放

3.栈区:数据由编译器管理开辟和释放 ,存放局部变量局部常量

​ 不要返回局部变量地址,栈区数据在函数执行完成后自动释放

4.堆区由程序员分配和释放,若程序员不释放,程序结束可能由OS回收。容易产生内存碎片

加载地址:程序放置的地址

运行地址:这个地址可以由编译器的编译参数来决定。

目标文件中各功能块(函数或变量)的相对地址:只是一个偏移量,它在编译阶段已经确定;

程序运行绝对地址:整个程序运行时的首地址,在连接阶段才确定。

2.五个基本原则:

单一职责原则(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
Liskov替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口

3.new malloc

  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

  • new 返回指定类型的指针,并且可以自动计算所需要大小

//比如:
int *p;   
p = new int; //返回类型为int* 类型(整数型指针),分配大小为 sizeof(int);   
//或:   
int* parr;   
parr = new int [100]; //返回类型为 int* 类型(整数型指针),分配大小为 sizeof(int) * 100;   123456
  • 而 malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针(void *)。
int* p;   
p = (int *) malloc (sizeof(int)*128);//分配128个(可根据实际需要替换该数值)整型存储单元,并将这128个连续的整型存储单元的首地址存储到指针变量p中  
double *pd=(double *) malloc (sizeof(double)*12);//分配12个double型存储单元,并将首地址存储到指针变量pd中123
  • malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。

  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

  • 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

  • 我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

  • 既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  • 如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

4.static、const

1.静态方法只能访问静态成员(包括成员变量和成员方法)

非静态方法可以访问静态也可以访问非静态

2.静态方法中不可以定义this,super关键字

因为 一个类中,一个static变量只会有一个内存空间,虽然有多个类实例,但这些类实例中的这个static变量会共享同一个内存空间。静态方法在优先于对象存在,所以静态方法中不可以出现this,super关键字。

3.主函数是静态的。

static局部变量 将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完成之后不会被释放,而是

继续保留在内存中

static 全局变量 表示一个变量在当前文件的全局内可访问

static 函数 表示一个函数只能在当前文件中被访问

static 类成员变量 表示这个成员为全类所共有,类内声明、类外初始化

static 类成员函数 表示这个函数为全类所共有,而且只能访问静态成员变量

const 常量:定义时就初始化,以后不能更改。

const 形参:func(const int a){};该形参在函数里不能改变

const修饰类成员函数:常函数,该函数对成员变量只能进行只读操作

5.指针、引用

指针是实体,引用是别名;

指针创建时会分配内存,引用不会;

指针定义时可以不用初始化,引用定义时必须初始化,而且之后不能修改;

指针可以为空,引用不能为空;

指针对数值操作时需要解引用(*),引用可以直接操作。

1.指针常量与常量指针

int *const p ;//指针常量	指向的地址不能被修改,但是地址里的内容可以被修改
p=&a;
*p=9;			//成功
p=&b;			//失败

const int *p ;//常量指针	指向的值不可以通过指针修改  
p=&a;
*p=9;			//失败
p=&b;			//成功

指针常量:是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。

常量指针:是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。

左定值,右定向

const在*左边,值不可更改

const在*右边,指向不可改

2.指针数组与数组指针

/*数组指针*/
int (*p)[n];
//()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。

//如要将二维数组赋给一指针,应这样赋值:
int a[3][4];
int (*p)[4]; 	//该语句是定义一个数组指针,指向含4个元素的一维数组。
 p=a;        	//将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
 p++;       	//该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

/*指针数组*/
int *p[n]
//[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。

//如要将二维数组赋给一指针数组:
int a[3][4];
int *p[3];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i]=a[i]
//这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]所以要分别赋值。

3.指针函数与函数指针

/*指针函数*/
//其本质是一个函数,而该函数的返回值是一个指针。
int *fun(int a,int b);

/*函数指针*/
//其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针 就是指向函数的指针。
int (*fun)(int a,int b);

一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数

int (*a[10])(int)

一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,函数的返回值是int,参数是int*

int (*(*a)[10])(int*)

6.线程和进程

线程是指进程内的一个执行单元,也是进程内的可调度实体。

(1)调度:线程作为cpu调度和分配的基本单位,进程是程序执行和资源分配的基本单位;

(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程也可以并发执行;

(3)拥有资源:进程是拥有资源的一个独立单元,线程不拥有系统资源但可以访问隶属于进程的资源;

(4)系统开销:在创建或撤销进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤销线程时的开销。

多进程会内存隔离,单个进程异常不会影响到整个程序,但是在调用、通讯和切换上开销比较大,
多线程可以提高系统并行性,开销小,但是单个线程异常会导致整个程序崩溃

多进程

1.处理僵尸进程

signal(SIGCHLD,SIG_IGN);

2.关闭多余的socket

tcp通信中,需要两个socket

一个用来监听,一个用来建立连接

父进程关闭connectsocket,子进程关闭listensocket

线程相关函数:

1.创建
int pthread_create(pthread_t *thread,
					const pthread_attr_t *attr,
					void *(*start_routine)(void*),
					void * arg);

参数:
thread:线程标识符地址。
attr:线程属性结构地址。
start_routine:线程函数的入口地址。
arg:传给新线程执行函数的参数。
返回值:
成功返回0,失败返回非0;

2.回收
int pthread_join(pthread_t thread, void **value_ptr);

参数:
thread:被等待的线程号。
value_ptr:指针的地址,调用此函数后,指针指向一个存储线程完整退出状态的静态区域,可用来存储被等待线程的返回值。
返回值:
成功返回0,失败返回非0。

3.分离
int pthread_detach(pthread_t thread);

功能:
使调用线程与当前进程分离,使其成为一个独立的线程,该线程终止时,系统将自动回收它的资源。
参数:
thread:线程ID
返回值:
成功:返回0,失败返回非0。

实例:

void *thread(void *arg)
{
	int i;
	for(i=0; i<5; i++)
	{
		printf("I am runing\n");
		sleep(1);
	}
	return NULL;
}
int main(int argc, char *argv[])
{
	pthread_t tid;	
	if(pthread_create(&tid, NULL, thread, NULL) != 0)
    {
        perror("create\n");
        return -1;
    }
	pthread_detach(tid1);//分离后独立,进程结束时线程也结束(3s后结束)
	pthread_join(tid, NULL);
	sleep(3);
	while(1);
	return 0;
}

4.取消
int pthread_cancel(pthread_t thread);

功能:
取消线程。
参数:
thread:目标线程ID。
返回值:
成功返回0,失败返回出错编号。

void *thread_cancel(void *arg)
{
	/*取消状态*/
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);//可被取消
	// pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);//不可被取消
	
	
	while(1)
	{
		/*取消点*/
		pthread_testcancel();
	}
	
	/*取消类型*/
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);//立即取消
	// pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);//不立即取消

	return NULL;
}

int main(int argc, char *argv[])
{
	int ret = 0;
	pthread_t tid1;

	ret = pthread_create(&tid1, NULL, thread_cancel, NULL);
	if(ret != 0)
	{
		perror("pthread_create");
	}
	sleep(3);
	pthread_cancel(tid1);
	pthread_join(tid1, NULL);
	return 0;
}

7.vi命令

撤销命令:u (windows下Ctrl+z)

撤销被撤销的命令:Ctrl+r (windows下Ctrl+y)

删除命令:x(向后删除)

删除行:数字+dd 3dd:删除三行 默认1行

可视模式:v 切换活动端:o (常用来选中区域复制)

8.私有化

构造函数私有化的类的设计保证了其他类不能从这个类派生或者创建类的实例

如果将构造函数设计成Protected,也可以实现同样的目的,但是可以被继承。

构造函数私有化的类的设计可以保证只能用new命令在堆中来生成对象,只能动态的去创建对象,这样可以自由的控制对象的生命周期。

把析构函数定义为私有的,就阻止了用户在类域外对析构函数的使用。这表现在如下两个方面:

​ 1. 禁止用户对此类型的变量进行定义,即禁止在栈内存空间内创建此类型的对象。要创建对象,只能用 new 在堆上进行。

​ 2. 禁止用户在程序中使用 delete 删除此类型对象。对象的删除只能在类内实现,也就是说只有类的实现者才有可能实现对对象的 delete,用户不能随便删除对象。如果用户想删除对象的话,只能按照类的实现者提供的方法进行。

查找

1.顺序查找

说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。

**基本思想:**顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。

复杂度分析:

顺序查找的时间复杂度为O(n)。

2.二分查找(折半查找)

元素必须是有序的,如果是无序的则要先进行排序操作。

**基本思想:**也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。

复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n)

#include<iostream>
using namespace std;
int find(int *array, int length, int target)
{
	int low = 0;
	int high = length - 1;
	while (low <= high)
	{
		int mid = low + (high - low)/2;
		if (target == array[mid])
			return array[mid];
		else if (target > array[mid])
			low = mid + 1;
		else if (target < array[mid])
			high = mid - 1;

	}
	return -1;
}

int main()
{
	int array[] = { 1,2,3,4,5,6,7,8 };
	int length = sizeof(array)/sizeof(array[0]);
	cout << length;
	int target = 8;
	cout << find(array, length, target);
	

	system("pause");
	return 0;
}


3.插值查找

**基本思想:**基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。

对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2n))

//基本思路:二分查找改进版,只需改一行代码。
//        mid=low+(key-a[low])/(a[high]-a[low])*(high-low)

排序

/*交换*/
void swap(int &a,int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

1.冒泡排序

每轮依次比较相邻两个数的大小,后面比前面小则交换

1.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2 复杂程度

时间复杂度O(n^2) 空间复杂度O(1)

空间复杂度O(n^2)

1.3 优化

加入一个标志位判断是否交换数据,在每一趟排序时初始化,如果未排序数列已经是有序数列,不需要交换数据,则直接退出排序。

void RubbleSort(int *arr, int len)
{
	if (len <= 1)
		return;
	for (int i = 0; i < len - 1; i++)
	{
		int tip = 0;//标志位
		for (int j = 0; j < len - i - 1; j++)
			if (arr[j] > arr[j + 1])
			{
				swap(arr[j],arr[j+1]);
				tip++;
			}
		if (tip == 0)
			return;
		cout << tip << endl;

	}
}

2.选择排序

从第一位开始,找到最小的数,放在第一位
从第二位开始,找到最小的数,放在第二位
……
每次排序,前面的数都是有序的

void SelectSort(int *arr,int len)
{
	if(len < 2)
        return;
    for(int i = 0;i<len-1;i++)
    {
        int min = i;
        for(int j = i+1;j<len;j++)
        {
            if(arr[j]<arr[min])
                min = j;
        }
        if(min != i)
            swap(arr[min],arr[i]);
    }
        
}

3.插入排序

从第二个数开始
从已排序的最右边开始,把大于当前排序的元素后移
每次循环,都保证最前面是有序的

void InsertSort(int *arr,int len)
{
    if(len < 2)
        return;
    for(int i = 1;i < len;i++)
    {
        int j;
        int temp = arr[i];
        for(j = i-1;(j>=0 && temp < arr[j]);j--)
        {
            arr[j+1] = arr[j];
        }
        arr[j+1] = temp;
    }
}

4.希尔排序

分组 插入排序的优化

void sort(int *arr,int len,int step,int first)
{
    for(int i = first + step;i<len;i+=step)
    {
        int j;
        int temp = arr[i];
        for(j = i-step;(j>=0 && arr[j]>temp);j-=step)
            arr[j+step] = arr[j];
        arr[j+step] = temp;
    }
}

void hillsort(int *arr,int len)
{
    if(len < 2)
        return;
    int step = len/2;
    while(step)
    {
        for(int i = 0;i<step;i++)
            sort(arr,len,step,i);
        step/=2;
    }
}

5.快速排序

void quicksort(int *arr,int len)
{
    if(len < 2)
        return;
    
    int left = 0,right = len-1;
    int flg = 1;
    int temp = arr[left];
    while(left < right)
    {
        if(flg == 1)
        {
            if(arr[right] > temp)
            {
                right--;
                continue;
            }
            arr[left] = arr[right];
            flg++;
            left++;
            continue;
        }
        if(flg == 2)
        {
            if(arr[left] < temp)
            {
                left++;
                continue;
            }
            arr[right] = arr[left];
            flg--;
            right--;
            continue;
        }
    }
    arr[left] = temp;
    quicksort(arr,left);
    quicksort(arr+left+1,len-left-1);
}

归并

7.堆排序

完全二叉树

N[i]的左节点:N[2i+1]

N[i]的右节点:N[2i+2]

N[i]的父节点:N[(i-1)/2]

从最后一个结点的父节点开始,使父节点大于子节点,形成一个大根堆。

让根节点与待排序的最后一个结点交换,根节点加入已排序的数组,

重复操作

/*
堆排序
start 待排序结点下标,
end 待排序数组最后一个元素下标
*/
void test20(int *arr, int start,int end)
{
	int father = start;
	int son = father * 2 + 1;

	//判断是否超出范围
	while (son <= end)
	{
		//先比较子节点大小,选出最大的
		if ((son + 1 <= end) && (arr[son] < arr[son + 1]))
			son++;

		//如果父节点大于子节点,跳出函数
		if (arr[father] > arr[son])
			return;
		swap(arr[father], arr[son]);
		father = son;
		son = father * 2 + 1;
	}
}

void test200(int *arr, int len)
{
	if (len < 2)
		return;
	for (int i = (len - 1) / 2; i >= 0; i--)
		test20(arr, i, len - 1);
	for (int i = len - 1; i > 0; i--)
	{
		swap(arr[0], arr[i]);
		test20(arr, 0, i - 1);
	}
}

8.计数排序

条件:

需要排序的元素是整数

元素取值再一定范围内,并且比较集中

int arrmax(int *arr, int len)
{
	int max = 0;
	for (int i = 0; i < len; i++)
		if (max < arr[i])
			max = arr[i];
	return max;
}

/*
计数排序
*/
void test201(int *arr, int len)
{
	if (len < 2)
		return;
	int max = arrmax(arr, len);
	int *arrtmp = new int[max + 1]();//开辟数组,并初始化为0

	for (int i = 0; i < len; i++)//将原数组的值,作为新数组的下标记录
		arrtmp[arr[i]]++;
	int tip = 0;
	for (int j = 0; j < max + 1; j++)
	{
		for (int k = 0; k < arrtmp[j]; k++)
			arr[tip++] = j;
	}

	delete[] arrtmp;
}

9.桶排序

分段排序,最后结合

10.基数排序

按位数切割成不同数字,然后按每个位数比较

/*
基数排序
*/
void sort1(int *arr, int len, int tip)
{
	int *result = new int[len];//存放从桶中收集后数据的临时数组
	int buckets[10] = { 0 };

	//遍历arr,将数据出现次数存储在buckets中
	for (int i = 0; i < len; i++)
		buckets[(arr[i] / tip) % 10]++;

	//调整buckets个元素的值,调整后的值就是arr中元素在result中的位置
	for (int i = 1; i < 10; i++)
		buckets[i] = buckets[i] + buckets[i-1];

	//将arr中元素填充到result中
	for (int i = len - 1; i >= 0; i--)
	{
		int exp = arr[i] / tip % 10;
		result[buckets[exp] - 1] = arr[i];
		buckets[exp]--;
	}
	memcpy(arr, result, len * sizeof(int));
	delete[] result;
}

void test400(int *arr, int len)
{
	if (len < 2)
		return;
	int max = arrmax(arr, len);
	int tip = 1;//排序指数,tip = 1 按个位排序;tip = 10 按十位排序
	while (max / tip)
	{
		sort1(arr, len, tip);
		tip *= 10;
	}
}

网络

1.字节序

大端:高序字节存储在起始地址

小端:低序字节存储在起始地址

0x12345678
大端:12 34 56 78
小端:78 56 34 12

网络字节序:大端

主机字节序:由cpu确定

转换函数:

16位无符号数:htons()、ntohs()

32位无符号数:htonl()、ntohl()

2.结构体

struct sockaddr
{
    unsigned short sa_family;	//地址类型
    char sa_data[14];			//14字节端口和地址
};


struct sockaddr_in
{
    short int sin_family;		//地址类型
    unsigned short int sin_port;//端口号
    struct in_addr sin_addr;	//地址
    unsigned char sin_zero[8];	//为了保持与struct sockaddr一样的长度
};

struct in_addr
{
    unsigned long s_addr;		//地址
};

3.相关库函数

1.socket

int socket(int domain,int type,int protocol);

参数:

​ domain:协议族,常用AF_INET(ipv4)、AF_INET6(ipv6)、AF_LOCAL

​ type:指定socket类型,常用SOCK_STREAM(流)、SOCK_DGRAM(数据报)

​ protocol:指定协议,常用IPPROTO_TCP、IPPROTO_UDP

一般情况,第一个参数AF_INET,第二个参数SOCK_STREAM,第三个参数0

socket(AF_INET,SOCK_STREAM,0);

返回值:

​ 成功返回一个socket,失败返回-1

问:一个系统最多创建几个socket?

答:文件描述符默认最多为1024,去掉0,1,2

2.bind

范围(1024-65535)

int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);

bind(sockfd,(struct sockaddr *)&my_addr,sizeof(my_addr))

参数:
sockfd: socket套接字
myaddr: 指向特定于协议的地址结构指针
addrlen:该地址结构的长度
返回值:
成功:返回0
失败:其他

int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);

3.listen

int listen(int sockfd, int backlog);

功能:
将套接字由主动修改为被动
使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接
参数:
sockfd: socket监听套接字
backlog:连接队列的长度
返回值:
成功:返回0
失败:其他

4.accept

int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);

功能:
从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待
参数:
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度
返回值:
已连接套接字(新的套接字,不是监听的套接字)

5.连接(服务器的端口号和ip地址)

int connect(int sockfd,const struct sockaddr *addr,socklen_t len);

功能:
主动跟服务器建立链接
连接建立成功后才可以开始传输数据(对于TCP协议)
参数:
sockfd:socket套接字
addr: 需连接的服务器地址结构
addrlen:地址结构体长度
返回值:
成功:0
失败:其他

6.发送

size_t send(int sockfd, const void* buf,size_t nbytes, int flags);

功能:
用于发送数据
注意:不能用TCP协议发送0长度的数据包
参数:
sockfd: socket套接字
buf: 待发送数据缓存区的地址
nbytes: 发送缓存区大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:
成功发送的字节数

7.接收

size_t recv(int sockfd, void *buf,size_t nbytes, int flags);

功能:
用于接收网络数据
参数:
sockfd: 套接字
buf: 指向接收网络数据的缓冲区
nbytes: 接收缓冲区的大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:
成功接收到字节数

完整读取数据,需要多次调用

socket报文格式:报文长度+报文内容

报文长度为4字节整数,表示报文内容的长度,tcp报文的长度是报文内容的长度+4

报文长度是4字节整数,以二进制流的方式写入socket,而不是ASCII

牛客

1.二维数组中的查找

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例1

输入:

7,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]

输出:

true
1)我么设初始值为右上角元素,arr[0][5] = val,目标tar = arr[3][1]
2)接下来进行二分操作:
3)如果val == target,直接返回
4)如果 tar > val, 说明target在更大的位置,val左边的元素显然都是 < val,间接 < tar,说明第 0 行都是无效的,所以val下移到arr[1][5]
5)如果 tar < val, 说明target在更小的位置,val下边的元素显然都是 > val,间接 > tar,说明第 5 列都是无效的,所以val左移到arr[0][4]
6)继续步骤2)

复杂度分析
时间复杂度:O(m+n) ,其中m为行数,n为列数,最坏情况下,需要遍历m+n次。
空间复杂度:O(1)
class Solution {
public:
    bool Find(int target, vector<vector<int> > array) {
        int m = array.size();
        int n = array[0].size();
        if(m == 0 || n == 0)
            return false;
        int i = 0;
        int j = n-1;
        while(i<m && j>=0)
        {
            if(target == array[i][j])
                return true;
            else if(target > array[i][j])
                i++;
            else 
                j--;
        }
        return false;
            
    }
};

易错:范围判断while(i<m && j>=0)

2.替换空格

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

分析:由于函数返回为void,说明此题不能另外开辟数组,需要in-place操作。我们知道字符串的遍历无非是从左到右和从右到左两种。
1)如果从左到右,会发现如果遇到空格,会将原来的字符覆盖。于是,此方法不行。
2)那么就考虑从右向左,遇到空格,就填充“20%“,否则将原字符移动应该呆的位置。

class Solution {
public: 
	void replaceSpace(char *str,int length) {
        if(str == NULL || length <= 0)
            return ;
        int tip = 0;
        for (int i = 0;i < length;i++)
        {
            if(str[i] == ' ')
                tip++;
        }
        int newLength = length+2*tip-1;
        for(int i = length-1;i>=0;i--)
        {
            if(str[i] == ' ')
            {
                str[newLength--] = '0';
                str[newLength--] = '2';
                str[newLength--] = '%';
            }
            else
                str[newLength--] = str[i];
        }
	}
};

易错:原数组第一个元素为空时的判断 i>=0

for(int i = length-1;i>=0;i--)

3.从尾到头打印链表

输入一个链表,按链表从尾到头的顺序返回一个ArrayList。

输入

{67,0,24,58}

输出

[58,24,0,67]

利用栈的先进后出来完成

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    vector<int> printListFromTailToHead(ListNode* head) {
        stack<int> s;
        vector<int> v;
        while(head)
        {
            s.push(head->val);
            head = head->next;
        }
        while(!s.empty())
        {
            v.push_back(s.top());
            s.pop();
        }
        return v;
    }
};

易错:指向下一个结点时的赋值与判断

while(head)
{
s.push(head->val);
head = head->next;
}

5.用两个栈实现队列

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

push操作就直接往stack1中push, pop操作需要分类一下:如果stack2为空,那么需要将stack1中的数据转移到stack2中,然后在对stack2进行pop,如果stack2不为空,直接pop就ok。
class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }

    int pop() {
        if(stack2.empty())
        {
            while(!stack1.empty())
            {
                stack2.push(stack1.top());
                stack1.pop();
            }                
        }
        int m = stack2.top();
        stack2.pop();
        return m;
    }

private:
    stack<int> stack1;
    stack<int> stack2;
};

6.旋转数组最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。

(二分查找)

输入:

[3,4,5,1,2]

输出:

1
class Solution {
public:
    int minNumberInRotateArray(vector<int> rotateArray) {
        if(rotateArray.empty())
            return 0;
        int low = 0,high = rotateArray.size()-1;
        while(low<high)
        {
            //确认数组是否非递减
            if(rotateArray[low]<rotateArray[high])
                return rotateArray[low];
            int mid = low + (high-low)/2;
            //左边数组为有序数组    
            if(rotateArray[mid]>rotateArray[low])
                low = mid+1;
            //右边数组为有序数组
            else if(rotateArray[mid]<rotateArray[high])
                high = mid;
            else
                ++low;            
        }
        return rotateArray[low];
    }
};

易错:二分查找的变形,判断结束的条件为 if(rotateArray[low]<rotateArray[high])

7.斐波那契数列

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。

输入

4

输出

3
//递归  耗时长
class Solution {
public:
    int Fibonacci(int n) {
        if(n == 0 || n == 1)
            return n;
        return Fibonacci(n-1)+Fibonacci(n-2);
    }
};

//使用三个临时变量记录
class Solution {
public:
    int Fibonacci(int n) {
        if(n == 0 || n == 1)
            return n;
        int a = 0,b = 1;
        int num = 0;
        for (int i = 2;i<=n;i++)
        {
            num = a+b;
            a = b;
            b = num;
        }
        return num;
    }
};

8.跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

输入

1
----
4

输出

1
-----
5

如果青蛙当前在第n级台阶上,那它上一步是在哪里呢?

显然,由于它可以跳1级台阶或者2级台阶,所以它上一步必定在第n-1,或者第n-2级台阶,也就是说它跳上n级台阶的跳法数是跳上n-1和跳上n-2级台阶的跳法数之和

class Solution {
public:
    int jumpFloor(int number) {
        if(number ==0 || number == 1)
            return number;
        int a = 1 ,b = 1, c = 0;
        for(int i = 2;i<=number;i++)
        {
            c = a+b;
            a = b;
            b = c;
        }
        return c;
    }
};

9.变态跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

示例1

输入:

3

输出:

4

每个台阶可以看作一块木板,让青蛙跳上去,n个台阶就有n块木板,最后一块木板是青蛙到达的位子,必须存在,其他 (n-1) 块木板可以任意选择是否存在,则每个木板有存在和不存在两种选择,(n-1) 块木板
就有 [2^(n-1)] 种跳法,可以直接得到结果。

其实我们所要求的序列为:0,1,2,4,8,16,……

所以除了第一位外,其他位的数都是前一位的数去乘以2所得到的积。

class Solution {
public:
    int jumpFloorII(int number) {
        if(number < 1)
            return -1;
        if(number == 1)
            return 1;
        else 
            return 2*jumpFloorII(number-1);

    }
};

10.矩阵覆盖

我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

比如n=3时,2*3的矩形块有3种覆盖方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RfFSwIuq-1614656232430)(E:\WorkBox\typora\images\59_1603852524038_7FBC41C976CACE07CB222C3B890A0995)]

解析:

假设:n块矩形有f(n)种覆盖方法。进行逆向分析,要完成最后的搭建有两种可能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8UeGCoqu-1614656232436)(E:\WorkBox\typora\images\7691123_1502859815612_8E28A27526ADD9D86EA67478A10E3C7A)]

第一种情况等价于情形1中阴影部分的n-1块矩形有多少种覆盖方法,为f(n-1);

第二种情况等价于情形2中阴影部分的n-2块矩形有多少种覆盖方法,为f(n-2);

故f(n) = f(n-1) + f(n-2),还是一个斐波那契数列。。。。

class Solution {
public:
    int rectCover(int number) {
        if(number == 1 || number == 0)
            return number;
        int a = 1,b = 1,c = 0;
        for(int i = 2;i<=number;i++)
        {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
        
    }
};

11.二进制中1的个数

输入一个整数,输出该数32位二进制表示中1的个数。其中负数用补码表示。

输入

10

输出

2
//二进制移位法
class Solution {
public:
     int  NumberOf1(int n) {
         int flg = 1;
         int cont = 0;
         while(flg)
         {
             if(flg & n)
                 cont++;
             flg <<= 1;
         }
         return cont;
     }
};

//-1
class Solution {
public:
     int  NumberOf1(int n) {
         int cont = 0;
         while(n)
         {
             n = n & (n-1);
             cont++;
         }
         return cont;
     }
};

易错:

1.设定一个标志位,让标志位右移,而不是让给定数左移

2.n = n & (n-1);每一次都是判断,不需要加if ()

12.数值的整数次方

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

保证base和exponent不同时为0

输入:

2,3

输出

8.00000

非递归快速幂

对于x的y次方,我们可以进行分类讨论:

当y为偶数时,xy=x(y/2)*x^(y/2);

当y为奇数时,xy=x(y/2)*x^(y/2)*x("/"表示整除)

于是,我们很容易就能想出一个O(log2m)的算法,也就是所谓的快速幂算法:

①建立一个long long类型的变量num来存储答案

②判断m的奇偶性,若为奇数,则将num乘上n

③n平方,m整除2

④若m不为0,则返回②

class Solution {
public:
    double Power(double base, int exponent) {
        if(exponent < 0)//次方为负数 
        {
            base = 1/base;
            exponent = -exponent;
        }
        double num = 1.0;//存储结果
        while(exponent)
        {
            if(exponent&1)
                num *= base;
            base *= base;
            exponent>>=1;
        }
        return num;
    }
};

13.调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

  1. 遇到偶数,++
  2. 遇到奇数a[i],插入到记录奇数标志位tip指向的位置a[tip],将[tip,i)整体向后移一位,同时tip++
class Solution {
public:
    void reOrderArray(vector<int> &array) {
        int tip = 0;
        for(int i = 0;i<array.size();i++)
        {
            if(array[i]&1)
            {
                int temp = array[i];
                for(int j = i;j>tip;j--)
                {
                    array[j] = array[j-1];
                }
                array[tip++] = temp;
            }
        }
    }
};

14.链表中倒数第k个结点

输入一个链表,输出该链表中倒数第k个结点。

输入

1,{1,2,3,4,5}

输出

{5}

使用快慢指针,首先让快指针先行k步,然后让快慢指针每次同行一步,直到快指针指向空节点,慢指针就是倒数第K个节点。

时间复杂度:O(n),不管如何,都只遍历一次单链表
空间复杂度:O(1)

class Solution {
public:
    ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
        if(pListHead == NULL || k <= 0)
            return nullptr;
        ListNode *high = pListHead;
        ListNode *slow = pListHead;
        while(k--)
        {
            if(high)
                high = high->next;
            else
                return nullptr;
        }
        while(high)
        {
            high = high->next;
            slow = slow->next;
        }
        return slow;
    }
};

对快指针加判断,链表长度与k的值比较

15.反转链表

class Solution {
public:
	struct ListNode* ReverseList(struct ListNode* pHead) {
        
		struct ListNode *pre = NULL;	//指向反转后的最后一个结点
		struct ListNode *cur = pHead;	//指向待反转的第一个结点
		struct ListNode *nex = NULL;	//指向待反转的第二个结点
		while (cur)
		{
			nex = cur->next;			//保存下一个数据
			cur->next = pre;			//当前结点下个指针指向反转链表最后一个节点
			pre = cur;					
			cur = nex;					//指针后移
		}
		return pre;
	}
};

最后返回的是 新的链表头结点

16.合并两个排序的链表

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

示例1

输入

{1,3,5},{2,4,6}

输出

{1,2,3,4,5,6}
  1. 如果l1指向的结点值小于等于l2指向的结点值,则将l1指向的结点值链接到cur的next指针,然后l1指向下一个结点值
  2. 否则,让l2指向下一个结点值
  3. 循环步骤1,2,直到l1或者l2为nullptr
  4. 将l1或者l2剩下的部分链接到cur的后面
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
    {
        ListNode *pHead = new ListNode(-1);
        ListNode *cur = pHead;
        while(pHead1 && pHead2)
        {
            if(pHead1->val >= pHead2->val)
            {
                cur->next = pHead2;
                pHead2 = pHead2->next;
            }
            else
            {
                cur->next = pHead1;
                pHead1 = pHead1->next;
            }
            cur = cur->next;
        }
        if(!pHead1)
            cur->next = pHead2;
        if(!pHead2)
            cur->next = pHead1;
        return pHead->next;
        
    }
};

18.二叉树的镜像

操作给定的二叉树,将其变换为源二叉树的镜像。

二叉树的镜像定义:源二叉树 
    	    8
    	   /  \
    	  6   10
    	 / \  / \
    	5  7 9 11
    	镜像二叉树
    	    8
    	   /  \
    	  10   6
    	 / \  / \
    	11 9 7   5
    	

先前序遍历这棵树的每个结点,如果遍历到的结点有子结点,就交换它的两个子节点,
当交换完所有的非叶子结点的左右子结点之后,就得到了树的镜像

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    void Mirror(TreeNode *pRoot) {
        if(pRoot == NULL)
            return ;      
        TreeNode *temp = pRoot->left;
        pRoot->left = pRoot->right;
        pRoot->right = temp;

        if(pRoot->left)
            Mirror(pRoot->left);
        if(pRoot->right)
            Mirror(pRoot->right);
    }
};


19.顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

输入

[[1,2],[3,4]]

输出

[1,2,4,3]

标记四个角的位置,每次打印一层,打印完后,向内收缩。

class Solution {
public:
    vector<int> printMatrix(vector<vector<int> > matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector<int> v;
        if(m == 0 || n == 0)
            return v;
        int left = 0,right = n-1,top = 0,bottom = m-1;
        while(left <= right && top <= bottom)
        {
            for (int i = left;i<=right;i++)
                v.push_back(matrix[top][i]);
            for (int j = top+1;j<=bottom;j++)
                v.push_back(matrix[j][right]);
            if(top == bottom)
                break;
            for (int i = right-1;i>=left;i--)
                v.push_back(matrix[bottom][i]);
            if(left == right)
                break;
            for (int j = bottom-1;j>top;j--)
                v.push_back(matrix[j][left]);
            top++,bottom--,left++,right--;
        }
        return v;
        
    }
};


打印完最上面和最右侧数值后,要判断是否为单行或单列数组,如果是,数组打印完毕,如果不是,继续打印。

20.包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

要求实现一个O(1)时间复杂度的返回最小值的栈。正常情况下,栈的push,pop操作都为O(1),
但是返回最小值,需要遍历整个栈,时间复杂度为O(n),所以这里需要空间换时间的思想。

  • push操作,s1直接入栈,s2如果没有数据就入栈;如果有数据,判断和栈顶数据大小,取小的做栈顶。
  • pop操作直接对应弹出就好了。
  • top操作就返回s1的栈顶
  • min操作就返回s2的栈顶
class Solution {
public:
    void push(int value) {
        s1.push(value);
        if(s2.empty())
            s2.push(value);
        else
        {
            if(s2.top() > value)
                s2.push(value);
            else
                s2.push(s2.top());
        }
    }
    void pop() {
        s1.pop();
        s2.pop();
    }
    int top() {
        return s1.top();
    }
    int min() {
        return s2.top();
    }
    stack<int>s1;
    stack<int>s2;
};

21.栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

输入

[1,2,3,4,5],[4,3,5,1,2]

输出

false
class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        stack<int> s;
        int i = 0,j = 0;
        while(i<pushV.size())
        {
            s.push(pushV[i++]);
            while(!s.empty()&&s.top() == popV[j])
            {
                s.pop();
                j++;
            }
        }
        return s.empty();
    }
};

34.第一个只出现一次的字符

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)

哈希

利用每个字母的ASCII码作hash来作为数组的index

class Solution {
public:
    int FirstNotRepeatingChar(string str) {
        if(str.empty())
            return -1;
        char a[128] = {0};
        for(int i = 0;i<str.size();i++)
            a[str[i]]++;
        for(int i = 0;i<128;i++)
            if(a[str[i]] == 1)
                return i;
        return -1;
    }
};

其他

1.string

class String
{
public:
	String(const char *str = NULL);// 普通构造函数  
	String(const String &other);// 拷贝构造函数  
	~String(void);// 析构函数  
	String & operator = (const String &other);// 赋值函数  
private:
	char *m_data;// 用于保存字符串  
};

String::String(const char *str)
{
    if(str == NULL)
    {
        m_data = new char[1];
        m_data='\0';        
    }
    else
    {
        int len = strlen(str);
        m_data = new char[len+1];
        strcpy(m_data,str);
    }
    
}
String::String(const String & str)
{
    int len = strlen(str.m_data);
    m_data = new char[len+1];
    strcpy(m_data,str.m_data);
}

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

String & String::operator(const String & str)
{
    if( this == &str)
    	return *this;
    if(m_data)
        delete [] m_data;
    int len = strlen(str);
    m_data = new char[len+1];
    strcpy(m_data,str.m_data);
    return *this;
}

2.字符串反向打印

string str;
cin >> str;
int n = str.size();
for (int i = n-1; i >= 0; i--)
{
    cout << str[i];
}

3 .写出一个程序,接受一个十六进制的数,输出该数值的十进制表示。

输入

0xA
0xAA

输出

10
170
#include <iostream>
using namespace std;

int main()
{
    int a;
    while(cin>>hex>>a)
    {
    	cout<<a<<endl;
    }
}


整理的部分面试题 ,如有错误,还望指出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值