计网
http & https
参考:https://www.cnblogs.com/gotodsp/p/6366163.html
参考:https://www.cnblogs.com/ranyonsue/p/5984001.html
参考:https://cyc2018.github.io/CS-Notes/#/notes/HTTP
- 请求、响应报文格式(4部分)
请求行,请求头部,空行,数据部分 - 长连接和短链接
请求响应完成后是否立即断开区分,1.0默认短连接,1.1默认长连接 - 请求响应过程
- 状态码
200,301,400,404,500,503 - 请求方法:
1.0:get, hed,post
2.0补充:put,delete,connect,options,trace - get/post区别:
(1)GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中.
(2)GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码.
(3)GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制.
select函数
参考:https://www.cnblogs.com/ccsccs/articles/4224253.html
函数原型:int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
- fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
- 理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
poll()函数
函数原型:int poll(struct pollfd * fds,unsigned int nfds ,int timeout);
先把服务器的描述符加入到描述符集合中,需要注意的是, select 所用的描述符集合是
一个fd_set 的结构体中,而poll 的描述符却是在一个以pollfd 为元素的数组中进行传递**
epoll()函数
参考:https://blog.csdn.net/to_be_better/article/details/47349573
- 函数原型:
epoll 操作过程需要3 个接口,分别如下:
int epoll_create(int size); // 创建一个epoll的句柄,size告诉内核要监听的数目
int epoll_ctl(int epfd , int op , int fd , struct epoll_event *event) ;//注册事件,ev结构体存放所有要监听的事件
int epoll_wait(int epfd, struct epoll _event * events, int maxevents, int
timeout);//等待事件发生,events存放发生了的事件 - 相对于select 和poll 来说, epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间之间的数据拷贝只需一次。
算法
递归算法时间复杂度
T(n)=a*T(n/b)+O(n^d)
- log(b,a) > d : O(n^log(b,a))
- log(b,a) =d : O(n^d *log(n))
- log(b,a)<d : O(n^d)
a:子问题个数
n/b:子问题规模
O(n^d):合并子问题的复杂度
排序稳定性
- 可以稳定:冒泡、插入、归并
- 不可以:选择、快排、堆排
二叉树
- 前、中、后、按层 非递归遍历,Z 遍历
- 深度、镜像 :递归
- 宽度:按层遍历
- 转链表:中序遍历
- 最低公共父节点、节点间的路径长度
- 打印从左边、右边看到的节点:按层遍历,打印每层第一个或最后一个
链表
-
主要用快慢指针、反转
-
删除涉及头节点时,可以申请新节点使得 newHead->next=head。这样可以将头节点当作普通节点操作,避免分类讨论。
-
删除到数第k个节点、删除所有重复节点
-
是否有环、环的入口
-
中位数:快慢指针,遍历一次,fast区分奇偶数目,(画图归纳)
-
反转
-
Y型相交:一是借助两个栈,可以返回相交点;二是反转其中一个,遍历第二个的 尾 是不是等于第一个原始的头
-
回文链表:快慢指针、反转后半部分
二分查找
注意循环条件,l、h赋值,返回值
- 正常查找
- 有重复时返回第一次的下标、最后一次的下标
- 开根号:正常查找
- 旋转数组最小值
图
- bfs、dfs遍历
- 最小生成树、最短路径
C++
线程锁
互斥锁、条件锁、自旋锁、读写锁
https://blog.csdn.net/bian_qing_quan11/article/details/73734157
INT_MAX INT_MIN
#include<limits.h>
int 最大值,最小值
宏函数和内联函数
宏只是进行简单的文本替换,而且是先替换再计算,会导致二义性。解决办法是把参数()起来,把结果()起来,比较麻烦
内联函数是真正的函数,采用参数传递
#include <iostream>
using namespace std;
#define sqr(x) (x*x)
int main()
{
int a,b=3;
a = sqr (b+2);
printf("%d",a);
return 0;
}
答案是11而不是25,原因是预编译宏展开的时候,被展开成了b+2*b+2。
将x括起来,改成如下程序
#include <iostream>
using namespace std;
#define sqr(x) ((x)*(x))
int main()
{int a,b=3;a = sqr (b+2);printf("%d",a);return 0;
}
则结果是25
typedef #define
typedef char *String_t; 和 #define String_d char *; 这两句在使用上有什么区别?
- 前者定义了一个新的类型别名,有类型检查;后者只是做了个简单的替换,无类型检查。
- 前者在编译的时候处理,后者在预编译的时候处理。
- 在使用上同时定义多个变量的时候有区别,如 String_t a, b; 或 String_d c, d; 其中 a, b 和 c 都是 char * 类型,而 d 为 char 类型;typedef 比 #define 安全,主要是 typedef 会做类型检查。
野指针是什么?
也叫空悬指针,不是指向null的指针,是指向垃圾内存的指针。
产生原因及解决办法:
- 指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
- 指针free或delete之后没有及时置空 => 释放操作后立即置空。
链接
软链接、硬链接、inode、连接数
参考:https://www.cnblogs.com/cherishry/p/5885107.html
参考:https://blog.csdn.net/bitboss/article/details/53940236
sizeof && strlen
sizeof()是运算符,获得对象的大小。当对象是指针式返回指针的大小(而不是指向的内存大小);当是数组时,返回数组所占的空间大小;当把数组作为实参传入时,返回指针大小。
strlen()是函数,返回字符串的字符数
int main()
{
char s1[]="hello";//计算末尾的‘\0'
char s2[5]={'a','b'};
char *s3="helll";
char *p=s2;
cout<<sizeof(s1)<<" "<<sizeof(s2)<<" "<<sizeof(s3)<<" "<<sizeof(p)<<endl;
return 0;
}
智能指针
参考 https://www.cnblogs.com/wxquare/p/4759020.html
new和malloc的区别
- 性质
new是操作符,操作符可以进行重载,malloc是库函数 - 使用
new使用的时候会自动计算大小,malloc则要指定大小
new/delete在对象(如类对象)创建的同时会【自动执行构造函数】做初始化,在对象在消亡时会自动执行析构函数。
而malloc只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的; - 成功
new成功后返回对象类型的指针,malloc返回void* 指针,需要自己做类型转换 - 失败
new抛出异常,malloc返回会NULL,所以用new之后在判断是否为NULL没什么意义 - 扩容
new不支持扩容, malloc在使用过程中发现内存不够可以使用realloc来进行扩容
const
左结合,左为空,右结合
- const修饰变量(包括指针)
- 修饰函数返回值,表示函数的返回值不能修改
- 修饰函数参数,表示该参数不能被改变
- 修饰函数
int get()const;
用在类中,表示函数仅能访问数据,而不能修改
static 作用
参考:https://www.cnblogs.com/songdanzju/p/7422380.html
- 限制全局变量的作用域:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
//a.c
char a = 'A'; // global variable
void msg()
{
printf("Hello\n");
}
//main.c
int main()
{
extern char a; // extern variable must be declared before use
printf("%c ", a);
(void)msg();
return 0;
}
结果是 A hello
- static的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生存期)
存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见
PS:如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。 - static 默认初始化为 0
- 在类中使用static
在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:
(1) 类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。
(2) 不能将静态成员函数定义为虚函数。
(3)静态成员初始化与一般数据成员初始化不同:
初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
数据类型><类名>::<静态数据成员名>=<值>
数组指针&&指针数组&&函数指针
数组指针:int (*p)[n];且()优先级高,首先说明p 是一个指针,且指向一个整型的一维
数组。这个一维数组的长度是n ,也可以说是p 的步长,也就是说执行p+l 时, p 要跨过n
个整型数据的长度。或者 int a[10] ; int *p=&a;
指针数组:int *p[n];p是一个数组,每个元素是一个指针。
函数指针:函数指针是指向函数的指针变量。所以,函数指针首先是个指针变量,而且这个变量指向一个函数。
#include<iostream>
using namespace std;
int max(int a,int b)
{
if(a>b)
return a;
return b;
}
int main()
{
int (*f)(int a,int b);//申明
f=max;//赋值
cout<<f(10,20)<<endl;
}
区别以下几种const变量
const int a;
int const a;
const int *a;
int *const a;
- int const a和const int a均表示定义常量类型a。
- const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)
- int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)
区别以下指针类型?
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
- int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
- int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
- int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
- int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
大端/小端
大端是内存低地址存高位数字
小端是内存低地址存地位数字
注意:一个字节存两个16进制数字
#include<iostream>
#include<stdio.h>
using namespace std;
union A{
char c[4];
int i;
};
int main()
{
cout<<sizeof(A)<<endl; //4
A a;
a.i=0x41424344;
printf("%x\n",a.i); //41424344
cout<<a.c<<endl; //DCBA
//证明系统是小端,因为i的值从左到右表示从高位到低位,c的输出是从低地址到到地址
}
结构体&类的区别
结构体的成员访问权限默认是public
而class默认是篇private
strcpy strncpy memcpy区别
strcpy:复制后加‘\0’,用于字符串
strncpy:遇到’\0’或达到size时结束,不加’\0’
memcpy:可用于数组/结构体等,用途广泛,void *memcpy(void *dest, const void src, size_t n); 起初,不能处理重叠,现在可以了。
memmove:支持内存地址重叠,其他都不支持,void memmove( void dest, const void src, size_t count );
void* my_memcpy(void* dst, const void* src, size_t n)//n是字节数
{
char *tmp_d = (char*)dst;
char *tmp_s = (char*)src;
if(tmp_d>tmp_s && tmp_s+n > tmp_d)//重叠
{
while(n--)
*(tmp_d+n-1)=*(tmp_s+n-1);
return dst;
}
while(n--)
{
*tmp_d++ = *tmp_s++;
}
return dst;
}
类
初始化列表更快
对于用户定义类型,使用列表初始化可以减少一次默认构造函数调用过程
class A{
private:
string s;
public:
A(string str)
{
s=str; //先调用string类的default构造函数,初始化s,
//再调用赋值函数将str赋值给s
}
A(string str)
:s(str) //直接使用str作为实参,调用string的拷贝函数进行拷贝构造s
{}
};
拷贝构造函数&&赋值
区别是对象是否存在
class A;
A=B;//赋值
class C=B;//拷贝构造函数
explicit关键字
参考:https://blog.csdn.net/qq_35524916/article/details/58178072
C++中, 只有一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色:
- 构造函数;
- 是个默认且隐含的类型转换操作符。
类型转换:我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造函数的参数类型, 这时候编译器就自动调用这个构造函数, 创建一个AAA的对象。
为了禁止这种做法,需要在构造函数前面加上explicit关键字,表明该构造函数只能被明确的调用,而不能用作类型转换。
虚函数实现和使用
参考1:https://www.cnblogs.com/malecrab/p/5572730.html
参考2: https://www.cnblogs.com/malecrab/p/5572730.html
- 执行构造函数时将虚函数的指针放到虚函数表里。如果是派生类,先复制基类的虚函数表,替换掉重写的虚函数地址,在后面加上自己实现的别的虚函数地址。若继承了多个基类,则会有多个虚函数表。
- 调用虚函数:pBase->show()
2.1 断Base类中show是否为虚函数。
2.2 若不是虚函数则找到pBase所指向的对象所属类Base。执行Base::show()。若是虚函数则执行步骤 2.3 .
2.3 访问pBase所指对象的虚函数表指针得到pBase所指对象所在类的虚函数表。
2.4 查找Base中show()在声明时的位序为x,到步骤3得到的虚函数表中找到位序x,从而得到要执行的show的函数地址。
2.5 根据函数地址和Base中声明的show的函数类型(形参和返回值)访问地址所指向的函数.
继承关键字
https://www.cnblogs.com/spring-hailong/p/6166191.html
虚继承
参考:https://blog.csdn.net/bxw1992/article/details/77726390
在菱形继承中,D会有两个A的副本,这使得:
- 在D中访问A的变量 a 时,不知道是哪一个,A->B->B 还是 A->C->D 指的 a, 产生歧义
- D包含两个A的副本是多余的,会浪费存储空间。
虚继承
class B:virtual public A
class C:virtual public A
class D:public B,C
- B、C会有虚基类表(类似虚函数表),存储虚基类相对于本类的偏移地址,保存指向表的虚基类指针。
- D继承时会继承B、C的虚基类指针,保留一份A的拷贝(我的理解是,D继承时通过两个虚基类指针访问表,再访问虚基类,发现是同一个基类,然后就只拷贝一份)
友元
- 友元函数 :
友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数); - 友元类 :
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名; - (1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元
const使用
- const数据成员要在初始化列表里初始化(还有 基类数据,子类数据,static数据,引用数据)
- const成员函数不能修改数据
- const对象的非const成员函数不能访问任何数据成员
- int get() 和 int get() const 是不一样的。
什么时候调用拷贝构造函数
- 用一个对象去初始化另一个对象
- 对象作为参数传递(此时是按值传递,要拷贝一个副本传递)
- 函数返回值是一个对象或引用时
空间大小
- 空类大小为 1
- static成员、成员函数、构造函数、析构函数不占空间
- 虚函数占用一个指针大小的空间(虚函数的指针)
构造函数、析构函数顺序
先基类或者子类的构造函数,当多重继承时,递归向上
再派生类或当前类
如 A -->B–>C, C c时,构造函数顺序:A(),B(),C()
析构函数顺序相反
构造函数不能为虚函数,析构函数可以而且常常是虚函数
this指针
在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this 指
针。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。例
如,当调用成员函数a.volume 时,编译系统就把对象a 的起始地址赋给this 指针,在成员
函数引用数据成员时,就按照this 的指向找到对象a 的数据成员。
静态数据
有时需要为某个类的所有对象分配一个单一的存储空间。在C 语言中,可以使用全局变
量,但这样很不安全。全局数据可以被任何人修改,而且在一个项目中,它很容易和其他名
字冲突。如果可以把数据当成全局变量那样去存储,但又被隐藏在类的内部,而且清楚地与
这个类相联系,这种处理方法就是最理想的。这个可以用类的静态数据成员来实现。**类的静
态成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据
成员都共享这一块静态存储空间,这就为这些对象提供了一种互相通信的方法。**静态数据成
员是属于类的,它只在类的范围内有效,可以是public 、private 或protected 的范围。
因为不管产生了多少对象,类的静态数据成员都有着单一的存储空间,所以存储空间必
须定义在一个单一的地方。如果一个静态数据成员被声明而没有被定义, 链接器会报告一个
错误:“ 定义必须出现在类的外部而且只能定义一次” 。因此静态数据成员的声明通常会放在
一个类的实现文件中。
STL
sort函数
- Sort函数包含在头文件为#include的c++标准库中:sort(begin,end,cmp)
- Sort函数有三个参数:
(1)第一个是要排序的数组的起始地址。
(2)第二个是结束的地址(最后一位要排序的地址+1)
(3)第三个参数是排序的方法,可以是从大到小也可是从小到大,还可以不写第三个参数,此时默认的排序方法是从小到大排序。
#include<iostream>
#include<algorithm>
using namespace std;
bool cmp(int a,int b)//比较函数,可以自定义,如升序,降序,结构体排序
{
return a>b;
}
int main()
{
int a[10]={9,6,3,8,5,2,7,4,1,0};
for(int i=0;i<10;i++)
cout<<a[i]<<" ";
cout<<endl;
sort(a,a+10);
for(int i=0;i<10;i++)
cout<<a[i]<<" ";
cout<<endl;
sort(a,a+10,cmp);
for(int i=0;i<10;i++)
cout<<a[i]<<" ";
return 0;
}
string
string int互转
- string转int
string s="123456";
int n=atoi(s.c_str());
- int 转string
/*#include<sstream>
int n=123456;
string s;
stringstream ss;
ss<<n;
ss>>s;*/
string s=to_string(n);
- 常用函数
str.substr(int start,int length);//子串
int str.find(int str1,int start);//str1是否是str得子串,是返回下标,否则返回-1
start是查找的起始位置,可省略
vector
基础知识
两个关键大小:
大小:size=_Mylast - _Myfirst;
容量:capacity=_Myend - _Myfirst;
分别对应于resize()、reserve()两个函数。
size表示vector中已有元素的个数,容量表示vector最多可存储的元素的个数;为了降低二次分配时的成本,vector实际配置的大小可能比客户需求的更大一些,以备将来扩充,这就是容量的概念。即capacity>=size,当等于时,容器此时已满,若再要加入新的元素时,就要重新进行内存分配,整个vector的数据都要移动到新内存。二次分配成本较高,在实际操作时,应尽量预留一定空间,避免二次分配。
使用resize(n)指定大小后,若再次push_back(),使得size>n时,后面的值会被抹去。而在初始化时指明大小,在超过时会扩容。
用法
定义:
vector<T> vec;
插入元素:
vec.push_back(element);
vec.insert(iterator, element);
删除元素:
vec.pop_back();
vec.erase(iterator);
//循环删除
for(it=map.begin();it!=map.end())
{
if(it->second == ...)
it=map.erase(it);
else
it++;
}
修改元素:
vec*[p*osition] = element;
遍历容器:
for(auto it = vec.begin(); it != vec.end(); ++it) {......}
其他:
vec.empty(); //判断是否空
vec.size(); // 实际元素
vec.capacity(); // 容器容量
vec.begin(); // 获得首迭代器
vec.end(); // 获得尾迭代器
vec.clear(); // 清空
map
基础知识
底层是红黑树,会自动排序,所以内部是有序的,插入删除的代价为O(log N),查找O(log N)
用法
定义:
map<T_key, T_value> mymap;
插入元素:
mymap.insert(pair<T_key, T_value>(key, value)); // 同key不插入
mymap.insert(map<T_key, T_value>::value_type(key, value)); // 同key不插入
mymap[key] = value; // 同key覆盖
删除元素:
mymap.erase(key); // 按值删
mymap.erase(iterator); // 按迭代器删
修改元素:
mymap[key] = new_value;
查找:
int f=mymap.count(key)//找到返回1,否则返回0
map<int ,string>::iterator it=mymap.find(key)//找到返回指向的迭代器,否则返回end();
遍历容器:
for(auto it = mymap.begin(); it != mymap.end(); ++it)
{
cout << it->first << " => " << it->second << '\n';
}
hash_map
基础知识
hash table 和 hash map 是类似的,只是前者是线程安全的,后者不是。
基本实现是 数组+链表
自定义数据类型时要重载 hash函数 和 比较函数
hash函数:得到数组下标,也就是桶号
比较函数:用于查找时确定时桶内的哪个节点
冲突解决
- 链地址法
- 开放地址检测
2.1 线性探测
2.2 平方探测 - 再散列
参考:https://blog.csdn.net/qq_32595453/article/details/80660676
为什么hash函数常见对素数取模
使得hash值均匀分布,降低冲突率。
常用的hash函数是选一个数m取模(余数),这个数在课本中推荐m是素数,但是经常见到选择m=2n,因为对2n求余数更快,并认为在key分布均匀的情况下,key%m也是在[0,m-1]区间均匀分布的。但实际上,key%m的分布同m是有关的。
证明如下: key%m = key - xm,即key减掉m的某个倍数x,剩下比m小的部分就是key除以m的余数。显然,x等于key/m的整数部分,以floor(key/m)表示。假设key和m有公约数g,即key=ag, m=bg, 则 key - xm = key - floor(key/m)m = key - floor(a/b)m。由于0 <= a/b <= a,所以floor(a/b)只有a+1中取值可能,从而推导出key%m也只有a+1中取值可能。a+1个球放在m个盒子里面,显然不可能做到均匀。
由此可知,一组均匀分布的key,其中同m公约数为1的那部分,余数后在[0,m-1]上还是均匀分布的,但同m公约数不为1的那部分,余数在[0, m-1]上就不是均匀分布的了。把m选为素数,正是为了让所有key同m的公约数都为1,从而保证余数的均匀分布,降低冲突率。
加载因子
所谓的加载因子就是 HashMap (当前的容量/总容量) 到达一定值的时候,HashMap 会实施扩容。加载因子也可以通过构造方法中指定,默认的值是 0.75 。举个例子,假设有一个 HashMap 的初始容量为 16 ,那么扩容的阀值就是 0.75 * 16 = 12 。也就是说,在你打算存入第 13 个值的时候,HashMap 会先执行扩容。