面经

单例模式

参考链接
作用:保证一个类只有一个实例,并提供一个访问它的全局访问点,使得系统中只有唯一的一个对象实例。

应用:常用于管理资源,如日志、线程池

实现要点:

在类中,要构造一个实例,就必须调用类的构造函数,并且为了保证全局只有一个实例,

需防止在外部调用类的构造函数而构造实例,需要将构造函数的访问权限标记为private

同时阻止拷贝创建对象时赋值时拷贝对象,因此也将它们声明并权限标记为private;

另外,需要提供一个全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。


class Singleton{
public:
static Singleton& getInstance(){
static Singleton instance;
return instance;
}
void printTest(){
cout<<"do something"<<endl;
}
private:
Singleton(){}//防止外部调用构造创建对象
Singleton(Singleton const &singleton);//阻止拷贝创建对象
Singleton& operator=(Singleton const &singleton);//阻止赋值对象
};
int main()
{
Singleton &a=Singleton::getInstance();
a.printTest();
return 0;

拷贝构造和拷贝赋值符是声明成了private而不给出定义,其目的是阻止拷贝

  • OSI七层模型,各是什么,分层原因
    分层的目的是利用层次结构可以把开放系统的信息交换问题分解到一系列容易控制的软硬件模块-层中,而各层可以根据需要独立进行修改或扩充功能,同时,有利于个不同制造厂家的设备互连,也有利于大家学习、理解数据通讯网络。
    OSI参考模型中不同层完成不同的功能,各层相互配合通过标准的接口进行通信。
    在这里插入图片描述在这里插入图片描述

tcp udp 头部, udp有包长度(65535),tcp没有

  我们在用Socket编程时,UDP协议要求包小于64K。TCP没有限定,TCP包头中就没有“包长度”字段,而完全依靠IP层去处理分帧。这就是为什么TCP常常被称作一种“流协议”的原因

在这里插入图片描述

/*TCP头定义,共20个字节*/
typedef struct _TCP_HEADER 
{
 short m_sSourPort;              // 源端口号16bit
 short m_sDestPort;              // 目的端口号16bit
 unsigned int m_uiSequNum;         // 序列号32bit
 unsigned int m_uiAcknowledgeNum;  // 确认号32bit
 short m_sHeaderLenAndFlag;        // 前4位:TCP头长度;中6位:保留;后6位:标志位
 short m_sWindowSize;            // 窗口大小16bit
 short m_sCheckSum;              // 检验和16bit
 short m_surgentPointer;           // 紧急数据偏移量16bit
}__attribute__((packed))TCP_HEADER, *PTCP_HEADER;

●源、目标端口号字段:占16比特。TCP协议通过使用"端口"来标识源端和目标端的应用进程。端口号可以使用0到65535之间的任何数字。在收到服务请求时,操作系统动态地为客户端的应用程序分配端口号。在服务器端,每种服务在"众所周知的端口"(Well-Know Port)为用户提供服务。

●顺序号字段:占32比特。用来标识从TCP源端向TCP目标端发送的数据字节流,它表示在这个报文段中的第一个数据字节。

●确认号字段:占32比特。只有ACK标志为1时,确认号字段才有效。它包含目标端所期望收到源端的下一个数据字节。

●头部长度字段:占4比特。给出头部占32比特的数目。没有任何选项字段的TCP头部长度为20字节;最多可以有60字节的TCP头部。

●标志位字段(U、A、P、R、S、F):占6比特。各比特的含义如下:

◆URG:紧急指针(urgent pointer)有效。

◆ACK:确认序号有效。

◆PSH:接收方应该尽快将这个报文段交给应用层。

◆RST:重建连接。

◆SYN:发起一个连接。

◆FIN:释放一个连接。

●窗口大小字段:占16比特。此字段用来进行流量控制。单位为字节数,这个值是本机期望一次接收的字节数。

●TCP校验和字段:占16比特。对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,并由目标端进行验证。

●紧急指针字段:占16比特。它是一个偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。

●选项字段:占32比特。可能包括"窗口扩大因子"、"时间戳"等选项。

UDP是一种不可靠的、无连接的数据报服务。源主机在传送数据前不需要和目标主机建立连接。数据被冠以源、目标端口号等UDP报头字段后直接发往目的主机。这时,每个数据段的可靠性依靠上层协议来保证。在传送数据较少、较小的情况下,UDP比TCP更加高效。
在这里插入图片描述

/*UDP头定义,共8个字节*/

typedef struct _UDP_HEADER 
{
 unsigned short m_usSourPort;       // 源端口号16bit
 unsigned short m_usDestPort;       // 目的端口号16bit
 unsigned short m_usLength;        // 数据包长度16bit
 unsigned short m_usCheckSum;      // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;

●源、目标端口号字段:占16比特。作用与TCP数据段中的端口号字段相同,用来标识源端和目标端的应用进程。

长度字段:占16比特。标明UDP头部和UDP数据的总长度字节

●校验和字段:占16比特。用来对UDP头部和UDP数据进行校验。和TCP不同的是,对UDP来说,此字段是可选项,而TCP数据段中的校验和字段是必须有的。

tcp udp 包大小限制

参考连接

考虑分片后的数据包大小

首先要看TCP/IP协议,涉及到四层:链路层,网络层,传输层,应用层。   
其中以太网(Ethernet)的数据帧在链路层   
IP包在网络层   
TCP或UDP包在传输层   
TCP或UDP中的数据(Data)在应用层   
它们的关系是 数据帧{IP包{TCP或UDP包{Data}}}

在这里插入图片描述
在应用程序中我们用到的Data的长度最大是多少,直接取决于底层的限制。
在应用程序中我们用到的Data的长度最大是多少,直接取决于底层的限制。   
我们从下到上分析一下:   
1.在链路层,由以太网的物理特性决定了数据帧的长度为(46+18)-(1500+18),其中的18是数据帧的头和尾,也就是说数据帧的内容最大为1500(不包括帧头和帧尾),即MTU(Maximum Transmission Unit)为1500;  
2.在网络层,因为IP包的首部要占用20字节,所以这的MTU为1500-20=1480; 
3.在传输层,对于UDP包的首部要占用8字节,所以这的MTU为1480-8=1472;   
所以,在应用层,你的Data最大长度为1472。当我们的UDP包中的数据多于MTU(1472)时,发送方的IP层需要分片fragmentation进行传输,而在接收方IP层则需要进行数据报重组,由于UDP是不可靠的传输协议,如果分片丢失导致重组失败,将导致UDP数据包被丢弃。   
从上面的分析来看,在普通的局域网环境下,UDP的数据最大为1472字节最好(避免分片重组)。   
但在网络编程中,Internet中的路由器可能有设置成不同的值(小于默认值),Internet上的标准MTU值为576,所以Internet的UDP编程时数据长度最好在576-20-8=548字节以内。

在这里插入图片描述

由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU(最大传输单元)

UDP 包的大小就应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes)
TCP 包的大小就应该是 1500 - IP头(20) - TCP头(20) = 1460 (Bytes)

不考虑分片,实际应用包大小:udp:sendto 65535 - 20 -8, tcp数据流+缓冲区无限制

    用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) - UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。  

    用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

tcp udp联系 区别

TCP与UDP区别总结:

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

TCP一般用于文件传输(FTP HTTP 对数据准确性要求高,速度可以相对慢),发送或接收邮件(POP IMAP SMTP 对数据准确性要求高,非紧急应用),远程登录(TELNET SSH 对数据准确性有一定要求,有连接的概念)等等;UDP一般用于即时通信(QQ聊天 对数据准确性和丢包要求比较低,但速度必须快),在线视频(RTSP 速度一定要快,保证视频连续,但是偶尔花了一个图像帧,人们还是能接受的),网络语音电话(VoIP 语音数据包一般比较小,需要高速发送,偶尔断音或串音也没有问题)等等。

tcp可靠传输

可以参考这个链接和腾讯那本书
1、确认和重传:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。

2、数据校验

3、数据合理分片和排序:

UDP:IP数据报大于1500字节,大于MTU.这个时候发送方IP层就需要分片(fragmentation).把数据报分成若干片,使每一片都小于MTU.而接收方IP层则需要进行数据报的重组.这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个UDP数据报.

tcp会按MTU合理分片,接收方会缓存未按序到达的数据,重新排序后再交给应用层。

4、流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
简单来说就是接收方处理不过来的时候,就把窗口缩小,并把窗口值告诉发送端。
5、拥塞控制:当网络拥塞时,减少数据的发送。TCP 的拥塞控制由 4 个核心算法组成:慢开始 、 拥 塞避免、快速重传和快速恢复 。

快重传:收到3个同样的确认就立刻重传,不等到超时;
快恢复:cwnd不是从1重新开始。

常见http状态码

http面试必看

1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求已被成功接收、理解、接受。
3xx:重定向–要完成请求必须进行更进一步的操作。
4xx:客户端错误–请求有语法错误或请求无法实现。
5xx:服务器端错误–服务器未能实现合法的请求。

常见状态代码、状态描述的说明如下。
200 OK:客户端请求成功。
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

滑动窗口

为什么要使用滑动窗口

因为发送端希望在收到确认前,继续发送其它报文段。比如说在收到0号报文的确认前还发出了1-3号的报文,这样提高了信道的利用率。但可以想想,0-4发出去后可能要重传,所以需要一个缓冲区维护这些报文,所以就有了窗口
窗口是什么

接收窗口:

在这里插入图片描述

“接收窗口”大小取决于应用(比如说tomcat:8080端口的监听进程)、系统、硬件的限制。图中,接收窗口是31~50,大小为20。

在接收窗口中,黑色的表示已收到的数据,白色的表示未收到的数据。

当收到窗口左边的数据,如27,则丢弃,因为这部分已经交付给主机;

当收到窗口左边的数据,如52,则丢弃,因为还没轮到它;

当收到已收到的窗口中的数据,如32,丢弃;

当收到未收到的窗口中的数据,如35,缓存在窗口中。

发送窗口:

在这里插入图片描述

发送窗口的大小swnd=min(rwnd,cwnd)。rwnd是接收窗口,cwnd用于拥塞控制,可与拥塞控制结合,暂时可以理解为swnd= rwnd =20。

图中分为四个区段,其中P1到P3是发送窗口。

tips:发送窗口以字节为单位。为了方便画图,图中展示得像以报文为单位一样。但这不影响理解。

进程线程的区别

区别

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

一个程序至少一个进程,一个进程至少一个线程。

为什么会有线程?

每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。

线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为改线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来。
进程分为单线程进程和多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作。

进程线程的区别:

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

  • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。
  • 两者均可并发执行。

排序的稳定性时间复杂度

算法 时间复杂度
最好 ---------- 平均 --------- 最坏
直接插入排序 o(n)-------- o(n的平方) ----------- o(n的平方)
冒泡排序 o(n)-------- o(n的平方) -------- o(n的平方)
选择排序 o(n的平方) -------- o(n的平方) -------- o(n的平方)
希尔排序 空--------o(nlogn)o(n的平方)----------o(nlogn)o(n的平方)
快速排序 o(nlogn)--------o(nlogn)--------o(n的平方)
堆排序 o(nlogn)--------o(nlogn)--------o(nlogn)
归并排序 o(nlogn)--------o(nlogn)--------o(nlogn)
基数排序 o(d(n+rd))--------o(d(n+rd))--------o(d(n+rd))

算法 空间复杂度
直接插入排序 o(1)
冒泡排序 o(1)
选择排序 o(1)
希尔排序 o(1)
快速排序 o(logn)
堆排序 o(1)
归并排序 o(n)
基数排序 o(rd)

算法 稳定性
直接插入排序 是
冒泡排序 是
选择排序 否
希尔排序 否
快速排序 否
堆排序 否
归并排序 是
基数排序 是

给定前序中序构建二叉树

思路:递归。

public class BuildBinaryTree {

    static class TreeNode {
        int val;
        TreeNode left = null;
        TreeNode right = null;

        TreeNode(int val) {
            this.val = val;
        }
    }

    // 根据先序和中序遍历构建二叉树
    // root用于preOrder[],begin和end用于inOrder[]
    public static TreeNode buildTree(int[] preOrder, int root,
                                      int[] inOrder, int begin, int end) {
        if (begin > end) return null;
        TreeNode node = new TreeNode(preOrder[root]);
        int loc, cnt = 0;
        for (loc = begin; loc <= end; loc++) {
            cnt++;
            if (preOrder[root] == inOrder[loc])
                break;
        }
        node.left = buildTree(preOrder, root + 1, inOrder, begin, loc - 1);
        node.right = buildTree(preOrder, root + cnt, inOrder, loc + 1, end);
        return node;
    }
}

树的层序遍历的递归和非递归做法

递归写法,递归参数是节点和层数。

    void dfs(TreeNode *node, int level)
    {
        if(node == NULL)
            return ;
        if(res.size() < level + 1)
            res.resize(level + 1);
        res[level].push_back(node -> val);
        dfs(node -> left, level + 1);
        dfs(node -> right, level + 1);
    }

非递归写法:
关键在于每层的处理,处理就是用到队列的长度。

   vector<vector<int>> res;
    if(!root) return res;
    queue<TreeNode*> qu;
    qu.push(root);
    while(!qu.empty())
    {
        vector<int> tmp;
        int len=qu.size();//队列的长度要注意
        for(int i=0;i<len;i++){
            TreeNode* node=qu.front();
            qu.pop();
            tmp.push_back(node->val);
            if(node->left) qu.push(node->left);
            if(node->right) qu.push(node->right);
        }
    res.push_back(tmp);
    }
    return res;
    }

不停删除子串

#include<stdio.h>
#include<string.h>
int main()
{
    int i,j,m;
    char a[81],b[81],*p;
    gets(a);
    gets(b);
    m=strlen(b);
    while((p=strstr(a,b))!=NULL) { //strstr 函数用于判断字符串str2是否是str1的子串。
        *p='\0';
        strcat(a,p+m); //将两个char类型连接。要求内存不重叠
    }
    puts(a);
    return 0;
}

如何实现一个不能被继承和拷贝的类

单例模式的实现:
子类的构造函数会调用基类的构造进行合成,要想一个类不被继承,只要把它的构造函数定义成私有,子类就没有办法访问基类构造函数,从而就阻止了进行子类构造对象
你把一个类的构造函数定义为私有的,那它自己也定义不出对象,这该如何解决。这时我们想到了静态函数,静态成员函数没有this指针,可以通过类直接访问,通过静态函数返回类的实例。

给定一块内存 如何在这块内存上调用类的构造函数

placement new, new的第三种形式,

class A
char* p=new char(sizeof(A));
A* q=new(p) A;

已知一个结构体一个成员变量的地址 如何知道这个结构体的地址(不知道成员变量的顺序的情况下)

已知b的地址求a的地址

struct A

{

int a;

int b;

int c;

}

struct A S;

&S = (unsigned int)&S.c - (unsigned int)&((struct A*)0)->c;

1 在0这个地址看做有一个虚拟的type类型的变量,那么取一个成员再取这个成员的地址,就是这个结构体中这个成员的绝对地址
2   (unsigned int)&((struct A*)0)->c这句话的意思是获取一个结构体中一个成员在这个结构体中的偏移。type *0是为了计算地址方便。
意思是在0这个地址看做有一个虚拟的type类型的变量,那么取一个成员再取这个成员的地址,就是这个结构体中这个成员的绝对地址,
由于结构体在地址为0的地方,所以这个成员在这个结构体中的相对位置也是这个值了。&s.c是这个member的指针,而现在想找这
个member所在结构体的地址,所以这个member的地址应该减去这个member在这个结构体中的偏移。然后返回这个结构体类型。

linux命令知道哪些 如何从用户态切换到内核态

a. 系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b. 异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中**,也就转到了内核态,比如缺页异常。**
c. 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

命令:

  • cd命令
  • ls命令  ls -l #以长数据串的形式列出当前目录下的数据文件和目录
  • grep命令,常与管道命令结合
    ls -l | grep -i file
    grep [-acinv] [--color=auto] '查找字符串' filename
# 取出文件/etc/man.config中包含MANPATH的行,并把找到的关键字加上颜色
grep --color=auto 'MANPATH' /etc/man.config
# 把ls -l的输出中包含字母file(不区分大小写)的内容输出
ls -l | grep -i file
  • find
  • find / -name passwd # 查找文件名为passwd的文件
  • cp命令 .复制文件
cp -a file1 file2 #连同文件的所有特性把文件file1复制成文件file2
cp file1 file2 file3 dir #把文件file1、file2、file3复制到目录dir中
  • mv命令 移动文件
mv file1 file2 file3 dir # 把文件file1、file2、file3移动到目录dir中
mv file1 file2 # 把文件file1重命名为file2
  • ps 该命令用于将某个时间点的进程运行情况选取下来并输出,process之意,它的常用参数如下:
-A :所有的进程均显示出来
-a :不与terminal有关的所有进程
-u :有效用户的相关进程
-x :一般与a参数一起使用,可列出较完整的信息
-l :较长,较详细地将PID的信息列出
ps -lA # 查看系统所有的进程数据

二叉树的镜像构建

递归方式:

void MirrorRecursively(TreeNode *pRoot)
{
    if((pRoot == NULL) || (pRoot->left == NULL && pRoot->right))
        return;

    TreeNode *pTemp = pRoot->left;
    pRoot->left = pRoot->right;
    pRoot->right = pTemp;
    
    if(pRoot->left)
        MirrorRecursively(pRoot->left);  

    if(pRoot->right)
        MirrorRecursively(pRoot->right); 
}

非递归方式:

void MirrorIteratively(TreeNode* pRoot)
{
    if(pRoot == NULL)
        return;

    std::stack<TreeNode*> stackTreeNode;
    stackTreeNode.push(pRoot);

    while(stackTreeNode.size() > 0)
    {
        TreeNode *pNode = stackTreeNode.top();
        stackTreeNode.pop();

        TreeNode *pTemp = pNode->left;
        pNode->left = pNode->right;
        pNode->right = pTemp;

        if(pNode->left)
            stackTreeNode.push(pNode->left);

        if(pNode->right)
            stackTreeNode.push(pNode->right);
    }
}

链表排序

归并排序

void mergearray(vector<int>&a, int first, int mid, int last)
{
	int i = first, j = mid + 1;
	int m = mid, n = last;
	int k = 0;
	vector<int> temp;
	while (i <= m && j <= n)
	{
		if (a[i] <= a[j])
			temp.push_back(a[i++]);
		else
			temp.push_back(a[j++]);
		k++;
	}

	while (i <= m)
	{
		temp.push_back(a[i++]); k++;
	}

	while (j <= n)
	{
		temp.push_back(a[j++]); k++;
	}

	for (i = 0; i <k; i++)
		a[first + i] = temp[i];
}
void mergesort(vector<int>&a, int first, int last)
{
	if (first < last)
	{
		int mid = (first + last) / 2;
		mergesort(a, first, mid);    //左边有序  
		mergesort(a, mid + 1, last); //右边有序  
		mergearray(a, first, mid, last); //再将二个有序数列合并  
	}
}

判断是否为质数

 if (n <= 3) {
        return n > 1;
    }
    for(int i = 2; i < n; i++){
        if (n % i == 0) {
            return false;
        }
    }
    return true;


    if (n <= 3) {
        return n > 1;
    }
    int sqrt = (int)Math.sqrt(n);
    for (int i = 2; i <= sqrt; i++) {
        if(n % i == 0) {
            return false;
        }
    }
    return true;

    // 不在6的倍数两侧的一定不是质数
    if (num % 6 != 1 && num % 6 != 5) {
        return false;
    }
    int sqrt = (int) Math.sqrt(num);
    for (int i = 5; i <= sqrt; i += 6) {
        if (num % i == 0 || num % (i + 2) == 0) {
            return false;
        }

汉诺塔问题

(1)以C盘为中介,从A杆将1至n-1号盘移至B杆;
(2)将A杆中剩下的第n号盘移至C杆;
(3)以A杆为中介;从B杆将1至n-1号盘移至C杆。

void hanoi(int n, char A, char B, char C)
{
    if (n == 1)
    {
        cout << A << "->" << C << endl;
        return;//递归终止
    }
    hanoi(n - 1, A, C, B);//将n-1个盘子从A移到B
    cout << A << "->" << C << endl;
    hanoi(n - 1, B, A, C);//将n-1个盘子从B移到C
    return;

}

主线程与工作线程

服务器端为了能流畅处理多个客户端链接,一般在某个线程A里面accept新的客户端连接并生成新连接的socket fd,然后将这些新连接的socketfd给另外开的数个工作线程B1、B2、B3、B4,这些工作线程处理这些新连接上的网络IO事件(即收发数据),同时,还处理系统中的另外一些事务。这里我们将线程A称为主线程,B1、B2、B3、B4等称为工作线程。工作线程的代码框架一般如下:


while (!m_bQuit)  {
    epoll_or_select_func();
 
    handle_io_events();
 
    handle_other_things();
}

这样做有三个好处:

线程A只需要处理新连接的到来即可,不用处理网络IO事件。由于网络IO事件处理一般相对比较慢,如果在线程A里面既处理新连接又处理网络IO,则可能由于线程忙于处理IO事件,而无法及时处理客户端的新连接,这是很不好的。

线程A接收的新连接,可以根据一定的负载均衡原则将新的socket fd分配给工作线程。常用的算法,比如round robin,即轮询机制,即,假设不考虑中途有连接断开的情况,一个新连接来了分配给B1,又来一个分配给B2,再来一个分配给B3,再来一个分配给B4。如此反复,也就是说线程A记录了各个工作线程上的socket fd数量,这样可以最大化地来平衡资源,避免一些工作线程“忙死”,另外一些工作线程“闲死”的现象。

即使工作线程不满载的情况下,也可以让工作线程做其他的事情。比如现在有四个工作线程,但只有三个连接。那么线程B4就可以在handle_other_thing()做一些其他事情。

下面讨论一个很重要的效率问题:

在上述while循环里面,epoll_or_selec_func()中的epoll_wait/poll/select等函数一般设置了一个超时时间。如果设置超时时间为0,那么在没有任何网络IO时间和其他任务处理的情况下,这些工作线程实际上会空转,白白地浪费cpu时间片。如果设置的超时时间大于0,在没有网络IO时间的情况,epoll_wait/poll/select仍然要挂起指定时间才能返回,导致handle_other_thing()不能及时执行,影响其他任务不能及时处理,也就是说其他任务一旦产生,其处理起来具有一定的延时性。这样也不好。

解决办法:

如果没有网络IO时间和其他任务要处理,那么这些工作线程最好直接挂起而不是空转。如果有其他任务要处理,这些工作线程要立刻能处理这些任务而不是在epoll_wait/poll/selec挂起指定时间后才开始处理这些任务。

我们采取如下方法来解决该问题,不管epoll_fd上有没有文件描述符fd,我们都给它绑定一个默认的fd,这个fd被称为唤醒fd。当我们需要处理其他任务的时候,向这个唤醒fd上随便写入1个字节的,这样这个fd立即就变成可读的了,epoll_wait()/poll()/select()函数立即被唤醒,并返回,接下来马上就能执行handle_other_thing(),其他任务得到处理。反之,没有其他任务也没有网络IO事件时,epoll_or_select_func()就挂在那里什么也不做。

  • 管道pipe,创建一个管道,将管道绑定到epoll_fd上
  • linux 2.6新增的eventfd:将生成的eventfd绑定到epoll_fd上。需要时,向这个 eventfd上写入一个字节,工作线程立即被唤醒。 int eventfd(unsigned int initval, int flags);
  • linux特有的socketpair,socketpair是一对相互连接的socket,相当于服务器端和客户端的两个端点,每一端都可以读写数据。 调用这个函数返回的两个socket句柄就是sv[0],和sv[1],在一个其中任何一个写入字节,在另外一个收取字节。

创建线程

pthread_create是UNIX环境创建线程函数

int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void**start_rtn)(void*),void *restrict arg);

返回值

若成功则返回0,否则返回出错编号

参数

第一个参数为指向线程标识符的指针。

第二个参数用来设置线程属性。

第三个参数是线程运行函数的地址。

最后一个参数是运行函数的参数。
注意:
 在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。

函数简介

函数pthread_join用来等待一个线程的结束。

函数原型为:

extern int pthread_join __P (pthread_t __th, void **__thread_return);

参数:

第一个参数为被等待的线程标识符

第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。

注意

这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。如果执行成功,将返回0,如果失败则返回一个错误号。

别人整理的百度算法

百度算法

深入理解tcp udp

三次握手四次挥手

在这里插入图片描述
三次握手目的:
加粗样式
其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息

(1)三次握手:握手过程中使用了 TCP 的标志(flag) —— SYN(synchronize) 和ACK(acknowledgement) 。

第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,完成三次握手。

若在握手过程中某个阶段莫名中断, TCP 协议会再次以相同的顺序发送相同的数据包。

三次握手目的:

两次的话,客户端发出连接请求,如果因为网络原因重发的话,服务端收到就以为是建立连接,发出确认报文段,但是客户端会忽略确认,也不发送数据,服务端则等待。

半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。

当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

SYN-ACK 重传次数
服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。

ISN(Initial Sequence Number)初始序号是固定的吗?
ISN随时间而变化,因此每个连接都将具有不同的ISN。ISN可以看作是一个32比特的计数器,每4ms加1 。

  • 这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。
  • 如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

三次握手可以携带数据吗?
**其实第三次握手的时候,是可以携带数据的。**但是,第一次、第二次握手不可以携带数据为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

SYN攻击是什么
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击**。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪**。SYN 攻击是一种典型的 DoS/DDoS 攻击。检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

常见的防御 SYN 攻击的方法有如下几种:

缩短超时(SYN Timeout)时间
增加最大半连接数
过滤网关防护
SYN cookies技术

(2)四次挥手:由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。先进行关闭的一方将执行主动关闭,而另一方被动关闭。

在这里插入图片描述

所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。
服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1
服务器B关闭与客户端A的连接,发送一个FIN给客户端A,序号与收到客户端的一样
客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。

在这里插入图片描述

挥手为什么需要四次?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。

这是由于tcp半连接决定的,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。 但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手

2MSL等待状态
每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。
为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。

滑动窗口 TCP使用滑动窗口机制来进行流量控制。

建立连接时,各端分配一个缓冲区用来存储接收的数据,并将缓冲区的尺寸发送给另一端。接收方发送的确认消息中包含了自己剩余的缓冲区尺寸。剩余缓冲区空间的数量叫做窗口。其实就是建立连接的双方互相知道彼此剩余的缓冲区大小。

在这里插入图片描述

拥塞控制, 慢开始

拥塞控制:防止过多的数据注入到网路中,这样可以使网络中的路由器或链路不至于阻塞。拥塞控制是一个全局性的过程,和流量控制不同,流量控制是点对点的控制。

1、慢开始:发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态的变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接收方的接收能力,发送窗口可能小于拥塞窗口。思路就是:不要一开始就发送大量的数据,先试探一下网络的拥塞程度,也就是说由小到大增加拥塞窗口的大小

在这里插入图片描述
2.拥塞避免:

拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按照线性规律缓慢增长。无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为⽆法判定,所以都当作拥塞处理),就把慢开始门限设置为出现拥塞时的发送窗口的一半,然后把拥塞窗口设置为1,执行慢开始算法。
此外,还有快速重传和快速恢复,停止-等待协议,回退N帧协议,选择重传协议等。
在这里插入图片描述
三、两者区别:

1) TCP提供面向连接的传输,通信前要先建立连接(三次握手机制); UDP提供无连接的传输,通信前不需要建立连接。
2) TCP提供可靠的传输(有序,无差错,不丢失,不重复); UDP提供不可靠的传输。
3) TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组; UDP是面向数据报的传输,没有分组开销。
4) TCP提供拥塞控制和流量控制机制; UDP不提供拥塞控制和流量控制机制

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

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

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

Connection:keep-alive

线程池 c++

线程池,可以多看两遍的
线程池的组成

1、线程池管理器
创建一定数量的线程,启动线程,调配任务,管理着线程池。
本篇线程池目前只需要启动(start()),停止方法(stop()),及任务添加方法(addTask).
start()创建一定数量的线程池,进行线程循环.
stop()停止所有线程循环,回收所有资源.
addTask()添加任务.

2、工作线程
线程池中线程,在线程池中等待并执行分配的任务.
本篇选用条件变量实现等待与通知机制.

3、任务接口,
添加任务的接口,以供工作线程调度任务的执行。

4、任务队列
用于存放没有处理的任务。提供一种缓冲机制
同时任务队列具有调度功能,高优先级的任务放在任务队列前面;本篇选用priority_queue 与pair的结合用作任务优先队列的结构.

线程池工作的四种情况.

二数之和,三数之和,四数之和

二数之和

       unordered_map<int,int> m;
 
        for(int i=0;i<nums.size();i++)
        {
            
            int tmp = target-nums[i];
            if(m.find(tmp)!=m.end())
            {
                return {m[tmp],i};
            }
            m[nums[i]] = i;
        }
        return {};

三数之和
三数之和,
1.将数组排序
2.定义三个指针,i,j,k。遍历i,那么这个问题就可以转化为在i之后的数组中寻找nums[j]+nums[k]=-nums[i]这个问题,也就将三数之和问题转变为二数之和—(可以使用双指针)

四数之和,注意去重

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());   //首先排序
        if (nums.empty()) return {};
        
        for(int z = 0; z < nums.size(); z ++){
            
            if (z > 0 && nums[z] == nums[z - 1]) continue;
            int newTarget = target - nums[z];   // 将四数之和转化为3数
            
            for(int k = z+1; k < nums.size(); k++){   // 三数变成两数
                
                if(k > z+1 && nums[k] == nums[k - 1]) continue;
                int newTarget2 = newTarget - nums[k];
                int i = k + 1, j = nums.size() - 1;
                while (i < j) {              // 两数之和直接套用《两数之和2》的题
                    if (nums[i] + nums[j] == newTarget2) {
                        res.push_back({nums[z], nums[k], nums[i], nums[j]});
                        while (i < j && nums[i] == nums[i + 1]) ++i;   //注意去重
                        while (i < j && nums[j] == nums[j - 1]) --j;
                        ++i; --j;
                    } else if (nums[i] + nums[j] < newTarget2) ++i;
                    
                    else --j;
                }
            }
            
        }
        return res;
        
    }
};

完全二叉树699节点求叶子节点数和层数

根结点的深度为1:

1.二叉树的第i层至多有2^(i − 1)个结点

2.深度为k的二叉树至多有2^k − 1个结点

因为2^9-1 < 699 < 2^10-1 ,所以这个完全二叉树的深度是10,前9层是一个满二叉树
这样的话,前九层的结点就有29-1=511个;而第九层的结点数是2(9-1)=256
所以第十层的叶子结点数是699-511=188个;
现在来算第九层的叶子结点个数。
由于第十层的叶子结点是从第九层延伸的,所以应该去掉第九层中还有子树的结点。因为第十层有188个,所以应该去掉第九层中的188 / 2=94个;
所以,第九层的叶子结点个数是256-94=162,加上第十层有188个,最后结果是350个。

查找第k小元素

int GetMinK(int A[],int n,int k)  
{  
    int s=-1,i=0,j=n-1,temp;  
    int beg=i;  
    int end=j;  
    while(s!=k)  
    {  
        beg=i;  
        end=j;  
        temp=A[i];  
        while(i<j)  
        {  
            while(i<j&&A[j]>=temp)j--;A[i]=A[j];  
                   while(i<j&&A[i]<=temp)i++;A[j]=A[i];  
        }  
         A[i]=temp;  
        s=i;  
   
     
        if(s==k)  
            return A[k];  
        if(s>k){i=beg;j--;} //在左侧寻找   
        if(s<k){j=end;i++;} //在右侧寻找   
    }  
}  

链表反转


/***非递归方式***/
node* reverseList(node* H)
{
    if (H == NULL || H->next == NULL) //链表为空或者仅1个数直接返回
        return H;
    node* p = H, *newH = NULL;
    while (p != NULL)                 //一直迭代到链尾
    {
        node* tmp = p->next;          //暂存p下一个地址,防止变化指针指向后找不到后续的数
        p->next = newH;               //p->next指向前一个空间
        newH = p;                     //新链表的头移动到p,扩长一步链表
        p    = tmp;                   //p指向原始链表p指向的下一个空间
    }
    return newH;
}
/***递归方式***/
node* In_reverseList(node* H)
{
    if (H == NULL || H->next == NULL)       //链表为空直接返回,而H->next为空是递归基
        return H;
    node* newHead = In_reverseList(H->next); //一直循环到链尾 
    H->next->next = H;                       //翻转链表的指向
    H->next = NULL;                          //记得赋值NULL,防止链表错乱
    return newHead;                          //新链表头永远指向的是原链表的链尾
}

最长公共子序列 子串

子序列

#include<bits/stdc++.h>
using namespace std;
int dp[2005][2005];
int main()
{
    char a[2005],b[2005];
    cin>>a>>b;
    for(int i=0;i<=strlen(a);i++)
    {
        dp[i][0]=0;
    }
    for(int j=0;j<=strlen(b);j++)
    {
        dp[0][j]=0;
    }
    for(int i=1;i<=strlen(a);i++)
    {
        for(int j=1;j<=strlen(b);j++)
        {
            if(a[i-1]==b[j-1])
            {
                dp[i][j]=dp[i-1][j-1]+1;
            }
            else
            {
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
    }
    cout<<dp[strlen(a)][strlen(b)]<<endl;
    return 0;
}

子串:

#include<bits/stdc++.h>
using namespace std;
int dp[2005][2005];
int main()
{
    char a[2005],b[2005];
    cin>>a>>b;
    for(int i=0;i<=strlen(a);i++)
    {
        dp[i][0]=0;
    }
    for(int j=0;j<=strlen(b);j++)
    {
        dp[0][j]=0;
    }
    for(int i=1;i<=strlen(a);i++)
    {
        for(int j=1;j<=strlen(b);j++)
        {
            if(a[i-1]==b[j-1])
            {
                dp[i][j]=dp[i-1][j-1]+1;
            }
            else
            {
                dp[i][j]=0;
            }
        }
    }
    int maxn=-1;
    for(int i=1;i<=strlen(a);i++)
    {
        for(int j=1;j<=strlen(b);j++)
        {
            maxn=max(maxn,dp[i][j]);
        }
    }
    cout<<maxn<<endl;
    return 0;
}

tcp可靠传输

TCP提供了可靠的传输服务,这是通过下列方式提供的:
分块发送:应用数据被分割成TCP认为最适合发送的数据块。由TCP传递给IP的信息单位称为报文段或段(segment)
定时确认重传:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒
数据校验:TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
正确排序:由于IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
重复丢弃:IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
流量控制:TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。

udp可靠传输,完成的太简单

udp 实现 tcp 的点对点方式
就是封包,校验,发送,确认,重新组包。
判断是否需要重发,这需要根据以前包的确认时间来推导本包

虚函数存在哪里

from: here

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

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

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

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

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

1亿url找前100个频率最高的

就是放到stl 的map里面对出现的频率作为pair的第二个字段进行排序,之后按照排序结果返回:

单链表有环

判断有环

在这里插入图片描述
对于这个问题我们可以采用“快慢指针”的方法。就是有两个指针fast和slow,开始的时候两个指针都指向链表头head,然后在每一步操作中slow向前走一步即:slow = slow->next,而fast每一步向前两步即:fast = fast->next->next。

由于fast要比slow移动的快,如果有环,fast一定会先进入环,而slow后进入环。当两个指针都进入环之后,经过一定步的操作之后

二者一定能够在环上相遇,并且此时slow还没有绕环一圈,也就是说一定是在slow走完第一圈之前相遇。

bool exitLoop(Node *head) 
{ 
    Node *fast, *slow ; 
    slow = fast = head ; 
   
    while (slow != NULL && fast -> next != NULL) 
    { 
        slow = slow -> next ; 
        fast = fast -> next -> next ; 
        if (slow == fast) 
            return true ; 
    } 
    return false ; 
} 

找出环的入口点

在这里插入图片描述
从上面的分析知道,当fast和slow相遇时,slow还没有走完链表,假设fast已经在环内循环了n(1<= n)圈。假设slow走了s步,则fast走了2s步,又由于

fast走过的步数 = s + n*r(s + 在环上多走的n圈),则有下面的等式:

2*s = s + n * r ; (1)

=> s = n*r (2)

如果假设整个链表的长度是L,入口和相遇点的距离是x(如上图所示),起点到入口点的距离是a(如上图所示),则有:

a + x = s = n * r; (3) 由(2)推出

a + x = (n - 1) * r + r = (n - 1) * r + (L - a) (4) 由环的长度 = 链表总长度 - 起点到入口点的距离求出

a = (n - 1) * r + (L -a -x) (5)

从链表起点head开始到入口点的距离a,与从slow和fast的相遇点(如图)到入口点的距离相等。
因此我们就可以分别用一个指针(ptr1, prt2),同时从head与slow和fast的相遇点出发,每一次操作走一步,直到ptr1 == ptr2,此时的位置也就是入口点!

如果存在环,求环上节点的个数

第一次相遇以后,第二次相遇fast比slow快一圈。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值