面试问题&日常学习

08.13 20:00 腾讯一面

(1) 代码,搜索插入位置???(二分法)

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int len=nums.size();
        if(len==0)
        return 0;
        if(nums[len-1]<target)
        return len;

        int start=0;
        int end=len-1;
        while(start<end)
        {
            int mid=(start+end)/2;
            if(nums[mid]<target)
            start=mid+1;
            else
            end=mid;
        }
return start;
    }
};

(2) 为了这次面试,做了哪些准备
学习
(3) 微信可以发送文字消息,这个功能点怎么测试
(4) Tcp为什么要三次握手
(5) 堆区和栈区的区别
(6) 问了数据库的内容,不会!
(7) 什么叫面向连接?tcp&udp

08.19 13:00 美团一面

(1)数据库了解吗???要根据市场需求去学习。
(2)“粘包”与“分包”有什么区别?
1、服务端与客户端没有约定好要使用的数据结构。Socket Client实际是将数据包发送到一个缓存buffer中,通过buffer刷到数据链路层。因服务端接收数据包时,不能断定数据包1何时结束,就有可能出现数据包2的部分数据结合数据包1发送出去,导致服务器读取数据包1时包含了数据包2的数据。这种现象称为粘包
2、数据包数据被分开一部分发送出去,服务端一次读取数据时可能读取到完整数据包的一部分,剩余部分被第二次读取。这种现象称为分包
3、解决方法:
a.定义一个稳定的结构。包头+length+数据包。包头用来防止 socket攻击,length用来获取数据包的长度。
b.在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。
TCP:由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象(确切来讲,对于基于TCP协议的应用,不应用包来描述,而应该用流来描述)。

UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。

解决方法
一个是采用分隔符的方式,即我们在封装要传输的数据包的时候,采用固定的符号作为结尾符(数据中不能含结尾符),这样我们接收到数据后,如果出现结尾标识,即人为的将粘包分开,如果一个包中没有出现结尾符,认为出现了分包,则等待下个包中出现后 组合成一个完整的数据包,这种方式适合于文本传输的数据,如采用/r/n之类的分隔符。
另一种是采用在数据包中添加长度的方式,即在数据包中的固定位置封装数据包的长度信息(或可计算数据包总长度的信息),服务器接收到数据后,先是解析包长度,然后根据包长度截取数据包(此种方式常出现于自定义协议中),但是有个小问题就是如果客户端第一个数据包数据长度封装的有错误,那么很可能就会导致后面接收到的所有数据包都解析出错(由于TCP建立连接后流式传输机制),只有客户端关闭连接后重新打开才可以消除此问题,我在处理这个问题的时候对数据长度做了校验,会适时的对接收到的有问题的包进行人为的丢弃处理(客户端有自动重发机制,故而在应用层不会导致数据的不完整性)

(3)如果有两个链表相交成字母Y,怎么找到交叉点?
剑指offer52题
第一种思路,暴力法,分别遍历两个数组,在第一个链表上的每一个结点,都在第二个链表上顺序遍历每一个,但是这样的时间复杂度就提升了,O(MN)。
第二种思路,可以用栈来辅助,先把两个链表都存入栈中,利用先进后出的规则,栈顶肯定是相同的元素,接着按顺序弹出,最后一个相同元素,就是要找的,但是这样占用了多余的空间。O(M+N).。
第三种思路,先遍历两个链表分别得到链表的长度,然后在长的链表中先走几步,直到找到第一个相同的结点,就是想要的结果。O(M+N).。

(4)为什么TCP是三次握手?(说多了)
为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
在这里插入图片描述
为了实现可靠传输,发送方和接收方始终需要同步( SYNchronize )序号。 需要注意的是, 序号并不是从 0 开始的, 而是由发送方随机选择的初始序列号 ( Initial Sequence Number, ISN )开始 。 由于 TCP 是一个双向通信协议, 通信双方都有能力发送信息, 并接收响应。 因此, 通信双方都需要随机产生一个初始的序列号, 并且把这个起始值告诉对方。于是, 这个过程就变成了下面这样。
在这里插入图片描述

(5)进程和线程的区别?
单CPU只能同时运行单个进程,多CPU可以同时运行多个进程。
一个进程可以包括多个线程。
内存共享:一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
内存安全:一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
互斥锁:防止两个线程同时读写某一块内存区域。
信号量:用来保证多个线程不会互相冲突。
操作系统的资源分配与调度逻辑:以多进程形式,允许多个任务同时运行;以多线程形式,允许单个任务分成不同的部分运行;提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
进程是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

理解它们的差别,我从资源使用的角度出发。(所谓的资源就是计算机里的中央处理器,内存,文件,网络等等)

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

开销方面:每个进程都有独立的代码和数据空间(程序上下文),进程之间切换开销大;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配:系统为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源

包含关系:线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

(6)在项目过程中学习到了什么?
(7)多路复用IO?
五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
a、用户线程使用同步阻塞IO模型的伪代码描述为:

{
	read(socket, buffer);
	process(buffer);
}

在这里插入图片描述
用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。
即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。
b、同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
在这里插入图片描述
用户线程使用同步非阻塞IO模型的伪代码描述为:

{
	while(read(socket, buffer) != SUCCESS);
	process(buffer);
}

即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。
c、IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
在这里插入图片描述
用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

用户线程使用select函数的伪代码描述为:

{
		select(socket);
		while(1) {
    	sockets = select();
		for(socket in sockets) {
		if(can_read(socket)) {
		read(socket, buffer);
		process(buffer);
}
}
}
}

其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。
d、“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。
(8)对重新换一种语言,学习思路是什么?
工具
所有语言中的这些工具都是相同的,你可以按照任意顺序学习,但是我通常会采用以下顺序:

(1)变量这似乎很简单,但话说回来,如何创建变量呢?

(2)运算符什么是运算符,如何使用?你明白基本的数学运算符,但是你明白逻辑运算符吗?“AND”运算符的正确拼写是“and”还是“AND”,还是使用诸如“&&”之类的符号?

(3)条件语句出人意料的是,我写的最受欢迎的Swift和Python文章都与决策有关。接下来,你需要知道如何在程序中做出决定。你学习的语言使用的是传统的“ if/else if/else”,还是像Python一样使用“ if/elif/else?你的语言是否带有“switch”或“guard”语句?

(4)循环语句如何遍历重复的任务?你学习的编程语言否包含for循环、while循环、do-while循环或for-each语句?

(5)函数是否可以创建函数?如果可以,那么该怎么创建?如何在这些函数中包含参数?你是否知道如何正确使用函数才能节省时间,并减轻你的工作负担?

(6)类和结构这种语言是否有类或结构的概念?这个问题听起来有点愚蠢,但有些语言要么没有,要么只有一种。如果有,那么该创建类或结构?类是否需要构造函数或初始化方法?

(7)错误处理错误是不可避免的。当出现错误时,这种语言是否拥有强大的错误处理解决方案,你又将如何使用呢?应该使用“ try/catch”、“ try/except”还是其他语句?是否还有其他子句,比如能进行其他处理的“else”或“finally”?

(8)测试如何测试代码?是否有用于测试的内置库,还是说你必须下载单独的工具? 大多数现代编程语言都包含上述工具。即使是稍旧的语言(例如COBOL)也拥有大多数工具,只不过它们有不同的称谓,例如pharagraph或copybook。

关于学习技术的建议:

1、关于编程手册

通常,代码正确性反映工程师对一门语言的掌握程度,是一个日积月累的过程。

遇到问题,首先看手册;通过搜索、浏览专业网站或者请教同事解决问题后,要看手册进行验证。

2、关于经典书籍。

每一门语言都会有好多不错的书籍,我们称为经典书籍。通过各种方法,找到一种或几种不错的书籍,认真阅读,不为完成任务的而阅读,细细品味地阅读,求精读而不求多读。

3、关于网络上的技术文章

网络上的技术性文章也是需要阅读的,但它不能代替书籍。

4、阅读语言的官方文档。如果希望在使用之前吸收大量信息,从阅读语言的参考资料中可能会受益。不用担心它们会对你催眠,参考文献通常是用于查找使用,而不是用来记忆。

5、使用互联网搜索。搜索网络是一个很好的方式,可提供有关特定错误和一般最佳做法的信息。还可以在网络中搜索有关解决语言中特定问题的最佳做法的博文。评估搜索结果中显示的博客帖子的质量和决定认真采用他们的建议时,请查看作者的公共代码组合以及发布日期。

6、与社区接触。虽然博客和新闻文章具有大量有用的信息,但是你尝试编写的特定代码片段总会有些微妙之处。不要害怕在邮件列表中发帖,或加入 IRC 和 Slack 频道以寻求帮助。

要提出有帮助的回复的问题,请确保在正确的地方提问。许多语言都有 “初学者” 邮件列表或聊天频道,专门针对可能会频繁询问的问题而建立。。

7、编写玩具程序。一次练习一个新的概念,很少有任务可打败只使用某个概念的玩具程序。你可以将重点放在尽可能让你的代码清洁和惯用性上。

8、了解你的问题和技术栈的特点

我们新学一门技术,往往是为了解决用现有技术栈不太容易解决的问题。因此,很有必要了解你面临的问题,看看解决问题的关键在哪里,可能的路径由多少。

9、列出待学习的技术点

熟悉了待解决的问题,选择了一个技术栈后,就要静下心来,进一步深入了解技术栈,看看究竟这条路上有多少技术点是必须要学的,把它们列出来,一定要列出来,这样才可以一个一个来学,不至于学着学着忘了这个漏了那个。

10、寻找合适的学习资料

互联网时代,知识盈余,信息过量,你想学什么东西,Google或百度一下,有关联的主题成千上万,没关联的主题万儿八千,总之信息浩如烟海,而我们却如落水的蚂蚁,实在有点浩淼水面终生难渡之感。
(9)链表和数组的区别?插入和删除,一定是链表快吗?
链表是一种上一个元素的引用指向下一个元素的存储结构,链表通过指针来连接元素与元素;链表是线性表的一种,所谓的线性表包含顺序线性表和链表,顺序线性表是用数组实现的,在内存中有顺序排列,通过改变数组大小实现。而链表不是用顺序实现的,用指针实现,在内存中不连续。意思就是说,链表就是将一系列不连续的内存联系起来,将那种碎片内存进行合理的利用,解决空间的问题。所以,链表允许插入和删除表上任意位置上的节点,但是不允许随即存取。链表有很多种不同的类型:单向链表、双向链表及循环链表。

不同:链表是链式的存储结构;数组是顺序的存储结构。

链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。

链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;

数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。
相同:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。
在这里插入图片描述

(10)有没有自己写过什么demo?主动去学习的新技术??
(11)线程池?
(12)什么叫哈希表,底层实现是什么?
1.哈希表的底层结构?
Java中的HashSet和HsahMap用的就是哈希表

数据结构决定了数据的检索,维护的效率。数组的检索效率高,增删元素的效率低。而链表的增删元素的效率高,检索元素的效率低。而哈希表结合了他们的优点。检索与增删效率都高。哈希表的底层结构就是一个数组,数组的长度即哈希表的长度,数组中的每个空间(也叫桶)存放的是一条链表,链表中的每个节点用来存放元素。即一个数组的每个数组元素是一条链表,链表的每个节点存放元素,可以将数组的每个元素看做桶,桶里面可以有多个元素。桶里元素直接的数据结构是链表。

2.哈希表如何检索元素?

哈希表首先根据要存放的元素的key得到hash值,将这个hash值经过"特殊计算的"结果,作为数组的下标(定位桶),进而确定将要遍历的链表。就不用遍历所有的链表,所以检索效率高。

3.哈希表如何增删元素?

首先一个哈希表的默认初始长度为16,即数组有16个空间(并非指能放16个元素)。默认加载因子为0.75。当向哈希表中添加元素的时候先判断是否要扩容,如果当前元素的总个数大于16*0.75那么要进行扩容,新的容量为原来的2倍。

在向哈希表中添加元素时首先通过要添加的元素的key的hash值,经过特殊算法计算的值作为下标,确定这个元素要放到数组中的那个桶里,然后遍历这个桶里的链表中的每个元素,与key进行equals判断如果equals返回false说明这个元素在哈希表中不存在则插入。如果返回true则不插入。

4.如何计算下标值?

通过要插入的元素的key调用hashcode方法得到哈希值,并与哈希表的长度减一进行与运算得到数组下标。
公式:hash(key)&table.length-1
在这里插入图片描述

09.07 14:00 美团测试

(1)支付金额修改输入框,(0,10],设计测试用例
(2)数据库语法,从表中更新一个人的成绩,查找
(3)写一个排序算法
(4)客户端发出请求到接收,Web耗时,输入url之后有哪些操作
(5)Equals和==号区别
在JVM中,内存分为堆内存跟栈内存。他们二者的区别是: 当我们创建一个对象(new Object)时,就会调用对象的构造函数来开辟空间,将对象数据存储到堆内存中,与此同时在栈内存中生成对应的引用,当我们在后续代码中调用的时候用的都是栈内存中的引用。还需注意的一点,基本数据类型是存储在栈内存中。

区别:
1、== 号是判断两个变量或实例是不是指向同一个内存空间,equals是判断两个变量或实例所指向的内存空间的值是不是相同
2、== 是指对内存地址进行比较 , equals()是对字符串的内容进行比较
3、== 指引用是否相同, equals()指的是值是否相同
在这里插入图片描述

(6)白盒测试的几种覆盖方法

10.24 16:00 京东零售

忘记了
1 项目学到了什么
2 计算机网络每一层,数据包转发
3 数据库,有没有用到索引
4 职业规划

。。。。。

11.20 小米嵌入式

项目
进程。线程。
线程间通信
答:调度算法
staic,抢占还是非抢占
freerots检测堆栈深度
堆和栈的区别
STM32F427和H系列区别

日常学习

1、IPC:进程间通信(Inter Process Communication)

指多个进程之间进行数据交换。进程间通信有4种方式,以下从简单到复杂的方式出场:
1.管道(pipe)
管道是一种具有两个端点的通信通道,一个管道实际上就是只存在在内存中的文件,对这个文件操作需要两个已经打开文件进行,他们代表管道的两端,也叫两个句柄,管道是一种特殊的文件,不属于一种文件系统,而是一种独立的文件系统,有自己的数据结构,根据管道的使用范围划分为无名管道和命名管道。
无名管道用于父进程和子进程之间,通常父进程创建管道,然后由通信的子进程继承父进程的读端点句柄和写端点句柄,或者父进程有读写句柄的子进程,这些子进程可以使用管道直接通信,不需要通过父进程。
命名管道,命名管道是为了解决无名管道只能在父子进程间通信而设计的,命名管道是建立在实际的磁盘介质或文件系统(而不是只存在内存中),任何进程可以通过文件名或路径建立与该文件的联系,命名换到需要一种FIFO文件(有先进先出的原则),虽然FIFO文件的inode节点在磁盘上,但仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。
2.信号
信号,用于接受某种事件发生,除了用于进程间通信之外,进程还可以发送信号给进程本身。除了系统内核和root之外,只有具备相同id的进程才可以信号进行通信。
3.消息队列
消息队列是消息的链表,包括Posix消息队列和system v消息队列(Posix常用于线程,system常用于进程),有权限的进程可以向消息队列中添加消息,有读权限的进程可以读走消息队列的消息。
消息队列克服了信号承载信息量少,管道只能承载无格式字节流及缓冲区大小受限等缺陷。
4.共享内存
共享内存使多个进程可以访问同一块内存空间,是最快的IPC形式,是针对其他通信方式运行效率低而设计的,往往与其他进程结合使用,如与信号量结合,来达到进程间的同步与互斥。传递文件最好用共享内存的方式。
工程中应用:
基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权限的程序。这些独立的任务之间的通讯与同步一般都是基于操作系统提供的IPC通讯机制,而FreeRTOS 中所有的通信与同步机制都是基于队列实现的。
消息队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传送信息,实现了任务接收来自其他任务或中断的不固定长度的消息。任务能够从队列里面读取消息,当队列中的消息是空时,挂起读取任务,用户还可以指定挂起的任务时间;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息,消息队列是一种异步的通信方式。FreeRTOS 任务间通信方式有 :

  • 消息通知 Notifications(V8.20版本开始支持)
  • 消息队列 Queues
  • 二进制信号量 Binary Semaphores
  • 计数信号量 Counting Semaphores
  • 互斥锁 Mutexes
  • 递归互斥锁 Recursive Mutexes

上面这几中方式中, 除了消息通知, 其他几种实现都是基于消息队列。消息队列作为主要的通信方式, 支持在任务间, 任务和中断间传递消息内容。
创建

// 定义队列句柄变量
QueueHandle_t xQueue;
// 申请队列
// 参数 1 : 队列深度
// 参数 2 : 队列项内容大小
xQueue = xQueueCreate( 10, sizeof( unsigned long ) );

发送&接收

void funOfTaskA()
{
    unsigned long pxMessage;
    // ...
    if( xQueue != 0 ) {
        // 发送消息
        // 参数 1 : 队列句柄
        // 参数 2 : 队列内容指针
        // 参数 3 : 允许阻塞时间
        xQueueSend( xQueue, ( void * ) &pxMessage, ( TickType_t ) 0 );
    }
}

void funOfTaskB()
{
    //... 
    // 接收消息
    // 参数 1 : 队列句柄
    // 参数 2 : 队列内容返回保存指针
    // 参数 3 : 允许阻塞时间
    if( xQueueReceive( xQueue, &( pxMessage ), ( TickType_t ) 10 ) )
        {
            // pcRxedMessage now points to the struct AMessage variable posted
            // by vATask.
        }
}

第一个函数发送消息到队列, 如果队列已经满了, 直接返回不阻塞。 第二个函数接收队列消息, 如果队列中没有消息, 会阻塞任务等待最长10个 Ticks。FreeRTOS 的队列内容是内存拷贝, 我们将要发送的内容的地址传递给发送函数,该函数会将地址上的内容拷贝到自己的存储区域;而接收函数会将消息内容拷贝到我们传递给他的指针指向的内存区域。

2、局域网

(1)关于ip地址
双向wifi就是,除了自己能够接收到无线网络信号外,还能将网络信号转化成WiFi分享给其他的移动终端设备。
工程应用
开发板自带WiFi模块,IP地址10.10.176.1,这是他自己作为热点的时候的地址,开发板是服务端TCPServer。电脑端sscom读取发出来的数据,需要设置成TCPClient,远程设置10.10.176.1,端口号4000。这时候,电脑使作为客户端,接收来自开发板WiFi发送来的数据,显示在窗口内。但是这个网络用于短程通信,无法连接互联网。
如果需要上网,可以在各个设备见搭建局域网。使用自己的手机开热点,作为服务端,电脑和开发板都作为客户端连接,手机会分配IP地址给这两个设备。其中开发板需要在电脑上配置,测试两个设备是否可以无线连接,用Dos窗口ping,看是否有返回,还是请求超时。比如,手机热点给开发板分配的ip地址是192.148.43.4,给电脑192.148.43.75,另一台电脑192.148.43.59,这几个移动终端就可以发消息通信了。

3、深度优先遍历(DFS)&广度优先遍历(BFS)

图的邻接矩阵,行列定义,是否可达。
BFS队列
DFS的非递归实现用的栈,是先序遍历的推广
DFS:从当前节点开始,先标记当前节点,再寻找与当前节点相邻,且未标记过的节点:
(1)当前节点不存在下一个节点,则返回前一个节点进行DFS
(2)当前节点存在下一个节点,则从下一个节点进行DFS
在这里插入图片描述
伪代码:

find(节点){
 
    if(此结点已经遍历 || 此节点在图外 || 节点不满足要求) return;

    if(找到了end节点) 输出结果 ; return;

    标记此节点,表示已经遍历过了;

    while(存在下一个相邻节点) find(下一个节点);

        }

BFS:以广度为优先的,一层一层搜索下去的,就像病毒感染,扩散性的传播下去。
在这里插入图片描述
这里需要用到队列:
a. 比如每遍历start周围的一个“1”节点的时候,就把跟它相关联的“2”节点保存到队列中(“1”节点访问完之后队列内容:2,2,2,2)
b. 然后依次访问队列内容,并对每个队列元素重复a步骤(访问一个“2”节点之后队列的内容:2,2,2,3,3)。
c. 由此重复下去,直到队列为空或者搜索到终点。
伪代码:

把start节点push入队列;
    while(队列不为空) {

        把队列首节点pop出队列;

        对节点进行相关处理或者判断;

        while(此节点有下一个相关节点){

            把相关节点push入对列;
        }
        }

4、queue

详细用法:
定义一个queue的变量 queue< Type > M
查看是否为空范例 M.empty() 是的话返回1,不是返回0;
从已有元素后面增加元素 M.push()
输出现有元素的个数 M.size()
显示第一个元素 M.front()
显示最后一个元素 M.back()
清除第一个元素 M.pop()

如果队列是两个数,要用pair。queue<pair<int,int>> neighbors;
最开始要pop

5、sort&set

对静态数组排序,默认升序,sort(a,a+10);
对vector排序,调用迭代器,默认升序,sort(a.begin(),a.end());
自定义排序,struct,bool
如果不只一个排序项目,最后else return false;

 1 #include<iostream>
 2 #include<vector>
 3 #include<algorithm>
 4 using namespace std;
 5 int main(){
 6     vector<int> a;
 7     int n = 5;
 8     while (n--){
 9         int score;
10         cin >> score;
11         a.push_back(score);
12     }
13     //cout <<" a.end()"<< *a.end() << endl;       执行这句话会报错!
14     cout << " prev(a.end)" << *prev(a.end()) << endl;
15     sort(a.begin(), a.end());
16     for (vector<int>::iterator it = a.begin(); it != a.end(); it++){
17         cout << *it << endl;
18     }
19     return 0;
20 }

自定义比较

struct student{
    char name[10];
     int score;
};
 //自定义“小于”
 bool comp(const student &a, const student &b){
     return a.score < b.score;
}

不只一个排序条件

 7 struct student{
 8     char name[10];
 9     int score;
10     int age;
11 };
12 //自定义“小于”
13 bool comp(const student &a, const student &b){
14     if (a.score > b.score)
15         return true;
16     else if (a.score == b.score  && a.age > b.age)
17         return true;
18     else                ///这里的else return false非常重要!!!!!
19         return false;
20 }

set是一个集合,内部的元素不会重复,同时它会自动进行排序,也是从小到大

而且set的insert方法没有insert(a,cmp)这种重载,所以如果要把结构体插入set中,我们就要重载’<'运算符。

set方法在插入的时候也是从小到大的,那么我们重载一下<运算符让它从大到小排序

 7 struct student{
 8     char name[10];
 9     int score;
10 };
11 //自定义“小于”
12 bool comp(const student &a, const student &b){
13     return a.score < b.score;
14 }
15 bool operator < (const student & stu1,const student &stu2){
16     return stu1.score > stu2.score;
17 }

6、URL到页面加载发生了什么

1、浏览器的地址栏输入URL并按下回车。
2、浏览器查找当前URL的DNS缓存记录。
3、DNS解析URL对应的IP。
4、根据IP建立TCP连接(三次握手)。
5、HTTP发起请求。
6、服务器处理请求,浏览器接收HTTP响应。
7、渲染页面,构建DOM树。
8、关闭TCP连接(四次挥手)。

详细点:
1、URL
我们常见的RUL是这样的:http://www.baidu.com,这个域名由三部分组成:协议名、域名、端口号,这里端口是默认所以隐藏。除此之外URL还会包含一些路径、查询和其他片段,例如:http://www.tuicool.com/search?kw=%E4%。我们最常见的的协议是HTTP协议,除此之外还有加密的HTTPS协议、FTP协议、FILe协议等等。URL的中间部分为域名或者是IP,之后就是端口号了。通常端口号不常见是因为大部分的都是使用默认端口,如HTTP默认端口80,HTTPS默认端口443。
2、DNS域名解析
域名只是与IP地址的一个映射。域名解析的过程实际是将域名还原为IP地址的过程。首先浏览器先检查本地hosts文件是否有这个网址映射关系,如果有就调用这个IP地址映射,完成域名解析。如果没找到则会查找本地DNS解析器缓存,如果查找到则返回。如果还是没有找到则会查找本地DNS服务器,如果查找到则返回。最后迭代查询,按根域服务器 ->顶级域,.cn->第二层域,hb.cn ->子域,www.hb.cn的顺序找到IP地址。
3、TCP
在通过第一步的DNS域名解析后,获取到了服务器的IP地址,在获取到IP地址后,便会开始建立一次连接,这是由TCP协议完成的,主要通过三次握手进行连接。第一次握手: 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认; 第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。完成三次握手,客户端与服务器开始传送数据。
4、浏览器向服务器发送HTTP请求
完整的HTTP请求包含请求起始行、请求头部、请求主体三部分。
5、缓存
我们说说浏览器缓存,HTTP缓存有多种规则,根据是否需要重新向服务器发起请求来分类,我将其分为强制缓存,对比缓存。
强制缓存判断HTTP首部字段:cache-control,Expires。
Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端时间可能不一致。因此该字段已经很少使用。
cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件后,缓存在484200s内均有效。 如果同时存在cache-control和  Expires,浏览器总是优先使用cache-control。
对比缓存通过HTTP的last-modified,Etag字段进行判断。
last-modified是第一次请求资源时,服务器返回的字段,表示最后一次更新的时间。下一次浏览器请求资源时就发送if-modified-since字段。服务器用本地Last-modified时间与if-modified-since时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送304状态码,让浏览器继续使用缓存。
Etag:资源的实体标识(哈希字符串),当资源内容更新时,Etag会改变。服务器会判断Etag是否发生变化,如果变化则返回新资源,否则返回304。
6、浏览器接收响应
服务器在收到浏览器发送的HTTP请求之后,会将收到的HTTP报文封装成HTTP的Request对象,并通过不同的Web服务器进行处理,处理完的结果以HTTP的Response对象返回,主要包括状态码,响应头,响应报文三个部分。
状态码主要包括以下部分
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。
响应头主要由Cache-Control、 Connection、Date、Pragma等组成。
响应体为服务器返回给浏览器的信息,主要由HTML,css,js,图片文件组成。
7、页面渲染
如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建DOM树和CSSOM树。
在浏览器还没接收到完整的 HTML 文件时,它就开始渲染页面了,在遇到外部链入的脚本标签或样式标签或图片时,会再次发送 HTTP 请求重复上述的步骤。在收到 CSS 文件后会对已经渲染的页面重新渲染,加入它们应有的样式,图片文件加载完立刻显示在相应位置。在这一过程中可能会触发页面的重绘或重排。这里就涉及了两个重要概念:Reflow和Repaint。
Reflow,也称作Layout,中文叫回流,一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树,这个过程称为Reflow。
Repaint,中文重绘,意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就OK了,这个过程称为Repaint。
所以说Reflow的成本比Repaint的成本高得多的多。DOM树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。

7、C++排序算法

1、分类
在这里插入图片描述
2、复杂度
在这里插入图片描述
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
3、快速排序
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(有序数组的话复杂度会上升,第一个基数随机选),接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边。在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6,递归对左右两个区间进行同样排序即可。
在这里插入图片描述
left指针,right指针,base参照数。

其实思想是蛮简单的,就是通过第一遍的遍历(让left和right指针重合)来找到数组的切割点。

第一步:首先我们从数组的left位置取出该数(20)作为基准(base)参照物。(如果是选取随机的,则找到随机的哨兵之后,将它与第一个元素交换,开始普通的快排)

第二步:从数组的right位置向前找,一直找到比(base)小的数,如果找到,将此数赋给left位置(也就是将10赋给20),此时数组为:10,40,50,10,60, left和right指针分别为前后的10。

第三步:从数组的left位置向后找,一直找到比(base)大的数,如果找到,将此数赋给right的位置(也就是40赋给10),此时数组为:10,40,50,40,60, left和right指针分别为前后的40。

第四步:重复“第二,第三“步骤,直到left和right指针重合,最后将(base)放到40的位置, 此时数组值为: 10,20,50,40,60,至此完成一次排序。

第五步:此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大, 以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。

//快速排序,随机选取哨兵放前面
void QuickSort(int* h, int left, int right)
{
    if(h==NULL) return;
    if(left>=right) return;

    //防止有序队列导致快速排序效率降低
    srand((unsigned)time(NULL));
    int len=right-left;
    int kindex=rand()%(len+1)+left;
    
    Swap(h[left],h[kindex]);

    int key=h[left],i=left,j=right;
    while(i<j)
    {
        while(h[j]>=key && i<j) --j;
        if(i<j) h[i]=h[j];
        while(h[i]<key && i<j) ++i;
        if(i<j) h[j]=h[i];
    }

    h[i]=key;

    //QuickSort(&h[left],0,i-1);
    //QuickSort(&h[j+1],0,right-j-1);

    QuickSort(h,left,i-1);
    QuickSort(h,j+1,right);
}

4、冒泡排序
冒泡排序在扫描过程中两两比较相邻记录,如果反序则交换,最终,最大记录就被“沉到”了序列的最后一个位置,第二遍扫描将第二大记录“沉到”了倒数第二个位置,重复上述操作,直到n-1 遍扫描后,整个序列就排好序了。

//冒泡排序
void BubbleSort(int* h, size_t len)
{
    if(h==NULL) return;
    if(len<=1) return;
    //i是次数,j是具体下标
    for(int i=0;i<len-1;++i)
        for(int j=0;j<len-1-i;++j)
            if(h[j]>h[j+1])
                Swap(h[j],h[j+1]);

    return;
}

5、选择排序
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

//选择排序
void SelectionSort(int* h, size_t len)
{
    if(h==NULL) return;
    if(len<=1) return;

    int minindex,i,j;
    //i是次数,也即排好的个数;j是继续排
    for(i=0;i<len-1;++i)
    {
        minindex=i;
        for(j=i+1;j<len;++j)
        {
            if(h[j]<h[minindex]) minindex=j;
        }
        Swap(h[i],h[minindex]);
    }

    return;
}

6、插入排序
直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。其基本思想如下:对于一个数组A[0,n]的排序问题,假设认为数组在A[0,n-1]排序的问题已经解决了。考虑A[n]的值,从右向左扫描有序数组A[0,n-1],直到第一个小于等于A[n]的元素,将A[n]插在这个元素的后面。很显然,基于增量法的思想在解决这个问题上拥有更高的效率。

直接插入排序对于最坏情况(严格递减的数组),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的数组),需要比较的次数是n-1,需要移位的次数是0。当然,对于最好和最坏的研究其实没有太大的意义,因为实际情况下,一般不会出现如此极端的情况。然而,直接插入排序对于基本有序的数组,会体现出良好的性能,这一特性,也给了它进一步优化的可能性。(希尔排序)。直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1),同时也是稳定排序。

下面用一个具体的场景,直观地体会一下直接插入排序的过程:
场景:
现有一个无序数组,共7个数:89 45 54 29 90 34 68。
使用直接插入排序法,对这个数组进行升序排序。

89 45 54 29 90 34 68

45 89 54 29 90 34 68

45 54 89 29 90 34 68

29 45 54 89 90 34 68

29 45 54 89 90 34 68

29 34 45 54 89 90 68

29 34 45 54 68 89 90

//插入排序
void InsertSort(int* h, size_t len)
{
    if(h==NULL) return;
    if(len<=1) return;

    int i,j;
    //i是次数,也即排好的个数;j是继续排
    for(i=1;i<len;++i)
        for(j=i;j>0;--j)
            if(h[j]<h[j-1]) Swap(h[j],h[j-1]);
            else break;

    return;
}
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页