C/C++ 编译采用分离编译模式。在一个项目中,有多个源文件存在,但是它们总会有一些相同的内容,比如用户自定义类型、全局变量、全局函数的声明等。将这些内容抽取出来放到头文件中,提供给各个源文件包含,就可以避免相同内容的重复书写,提高编程效率和代码安全性。所以,设立头文件的主要目的是:提供全局变量、全局函数的声明或公用数据类型的定义,从而实现分离编译和代码复用。
https://blog.csdn.net/K346K346/article/details/48877773
Resource Acquisition Is Initialization是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化。要解决的是这样一个问题:
在C++中,如果在这个程序段结束时需要完成一些资源释放工作,那么正常情况下自然是没有什么问题,但是当一个异常抛出时,释放资源的语句就不会被执行。 于是 [Bjarne Stroustrup] 就想到确保能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为 stack winding 会保证它们的析构函数都会被执行。
将初始化和资源释放都移动到一个包装类中的好处:
https://www.zhihu.com/question/22111546
#include
using namespace std;
#define my_swap(type,a,b) do{\
type tp;\
tp = a; \
a = b; \
b = tp; \
}while(0)
int main(){
int n = 10,m = 90;
printf("n: %d m: %d\n", n,m);
my_swap(int,n,m);
printf("n: %d m: %d\n", n,m);
return 0;
}
可能会冲突,比如用了using namespace std;
,然后全局变量来一个count,用 ::count
会报错ambiguous
const 是 constant 的缩写,本意是不变的,不易改变的意思。在 C++ 中是用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
–具体的有点多,所以看参考吧,但是面试可不能这样说,可以还是得自己凝练总结一下
C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
我看到用得比较多的是
https://www.runoob.com/w3cnote/cpp-const-keyword.html
参考回答:
静态存储区,在整个程序运行期间一直存在。
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文件结尾。
在局部变量之前加上关键字static,局部变量就成为一个局部静态变量。
内存中的位置:静态存储区
初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化);
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰;
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
变长结构体的作用:
为何不用指针代替变长结构体?
指针与变长结构体的区别:
#include
using namespace std;
#define rep(i, a, b) for(int i = int(a); i <= int(b); ++i)
struct v_struct {
int i;
int a[0];
};
struct msg { char cmd; char len; int extraData[0]; };
int main(){
v_struct *pv = (v_struct *)malloc(sizeof(v_struct)+sizeof(int)* 100);
pv->a[50] = 100;
// 我故意访问越界了一个,但是没有报错是什么鬼_100改成101试试
// rep(i,0,101) pv->a[i]=i;
rep(i,0,100) pv->a[i]=i;
int cnt=0;
rep(i,0,100){
cnt++;
cout<<pv->a[i]<<" ";
if(cnt%10==0) puts("");
}
cout<<"\n可以访问pv->a[100],可能是原来的'/0'标识处,但是不能访问pv->a[101]"<<endl;
// 2020年6月5日16:43:53 第二次探究,发现又可以访问pv->a[101]
pv->a[101] = 101;
cout<<"error here: pv->a[101]: "<<pv->a[101]<<endl;
printf("sizeof int is equal to %u\n", (unsigned int)sizeof(int));
printf("sizeof v_struct is equal to %u\n", (unsigned int)sizeof(v_struct));
// 64bit电脑输出8
printf("%u\n", (unsigned int)sizeof(pv));
struct msg *clientMsg = (struct msg *)malloc(sizeof(struct msg));
clientMsg->cmd = 'l';
clientMsg->len = 12;
cout<< " sizeof(struct msg): " << sizeof(struct msg) << endl;
cout<< " sizeof(struct msg*): " << sizeof(struct msg*) << endl;
cout<< "before realloc , the sizeof(*clientMsg): " << sizeof(*clientMsg) << endl;
clientMsg = (struct msg *)realloc(clientMsg, sizeof(struct msg) + 12 );
cout<< "sizeof(char): " << sizeof(char) << endl;
cout<< "after realloc , the sizeof(*clientMsg): " << sizeof(*clientMsg) << endl;
*(clientMsg->extraData) =11;
cout<< "after realloc , add one , the sizeof(*clientMsg): " << sizeof(*clientMsg) << endl;
*(clientMsg->extraData + 1) = 22;
cout<< "after realloc , add two , the sizeof(*clientMsg): " << sizeof(*clientMsg) << endl;
*(clientMsg->extraData + 3) = 33;
cout<< "after realloc , add and exceed. the sizeof(*clientMsg): " << sizeof(*clientMsg) << endl;
return 0;
}
#include
#include
using namespace std;
struct s_one {
int s_one_cnt;
char* s_one_buf;
};
struct s_two {
int s_two_cnt;
char s_two_buf[0];
};
struct s_three {
int s_three_cnt;
char s_three_buf[1];
};
int main() {
//赋值用
const char* tmp_buf = "abcdefghijklmnopqrstuvwxyz";
int ntmp_buf_size = strlen(tmp_buf);
//<1>注意s_one 与s_two的大小的不同
cout << "sizeof(char *) = " << sizeof(char *) << endl; // 8
cout << "sizeof(s_one) = " << sizeof(s_one) << endl; // 8+8
cout << "sizeof(s_two) = " << sizeof(s_two) << endl; // 4
cout << "sizeof(s_three) = " << sizeof(s_three) << endl; // 5-->8结构体对齐
cout << endl;
//为buf分配 sizeof(tmp_buf) 个字节大小的空间
int ntotal_stwo_len = sizeof(s_two) + (1 + ntmp_buf_size) * sizeof(char);
int ntotal_sthree_len = sizeof(s_three) + ntmp_buf_size * sizeof(char);
//给s_one buf赋值
s_one* p_sone = (s_one*)malloc(sizeof(s_one));
memset(p_sone, 0, sizeof(s_one));
p_sone->s_one_buf = (char*)malloc(1 + ntmp_buf_size);
memset(p_sone->s_one_buf, 0, 1 + ntmp_buf_size);
memcpy(p_sone->s_one_buf, tmp_buf, ntmp_buf_size);
//给s_two buf赋值
s_two* p_stwo = (s_two*)malloc(ntotal_stwo_len);
memset(p_stwo, 0, ntotal_stwo_len);
memcpy((char*)(p_stwo->s_two_buf), tmp_buf,
ntmp_buf_size); //不用加偏移量,直接拷贝!
//给s_three_buf赋值
s_three* p_sthree = (s_three*)malloc(ntotal_sthree_len);
memset(p_sthree, 0, ntotal_sthree_len);
memcpy((char*)(p_sthree->s_three_buf), tmp_buf, ntmp_buf_size);
cout << "p_sone->s_one_buf = " << p_sone->s_one_buf << endl;
cout << "p_stwo->s_two_buf = " << p_stwo->s_two_buf << endl;
cout << "p_sthree->s_three_buf = " << p_sthree->s_three_buf
<< endl; //不用加偏移量,直接拷贝!
cout << endl;
//<2>注意s_one 与s_two释放的不同!
if (NULL != p_sone->s_one_buf) {
free(p_sone->s_one_buf);
p_sone->s_one_buf = NULL;
if (NULL != p_sone) {
free(p_sone);
p_sone = NULL;
}
cout << "free(p_sone) successed!" << endl;
}
if (NULL != p_stwo) {
free(p_stwo);
p_stwo = NULL;
cout << "free(p_stwo) successed!" << endl;
}
if (NULL != p_sthree) {
free(p_sthree);
p_sthree = NULL;
cout << "free(p_sthree) successed!" << endl;
}
return 0;
}
https://blog.csdn.net/hust_hqq/article/details/79603288
https://blog.csdn.net/laoyang360/article/details/11908731
C++成员函数的重载、覆盖与隐藏详解
MTU(Maximum Transmission Unit)最大传输单元,在TCP/IP协议族中,指的是IP数据报能经过一个物理网络的最大报文长度,其中包括了IP首部(从20个字节到60个字节不等),一般以太网的MTU设为1500字节,加上以太帧首部的长度14字节,也就是一个以太帧不会超过1500+14 = 1514字节。
如上图所示,MTU指的都是一个物理网络之中的。在以太网中,如果上层协议交给IP协议的内容实在是太多,使得一个以太帧超过了1514字节,那么IP报文就必须要分片传输,到达目的主机或目的路由器之后由其重组分片。
MSS(Maximum Segment Size,最大报文段大小,指的是TCP报文(一种IP协议的上层协议)的最大数据报长度,其中不包括TCP首部长度。MSS由TCP链接的过程中由双方协商得出,其中SYN字段中的选项部分包括了这个信息。如果MSS+TCP首部+IP首部大于MTU,那么IP报文就会存在分片,如果小于,那么就可以不需要分片正常发送。
一般来说,MSS = MTU - IP首部大小 - TCP首部大小
https://blog.csdn.net/LoseInVain/article/details/53694265
为啥要mac:
MAC地址是物理地址,工作在数据链路层,一旦出厂时由厂商确定并烧制入网络设备的EPROM中就具有了固定的全球唯一的地址,任何时候任何条件都不会改变,虽说使用起来不太方便,且描述的是较低层的数据链路通信细节,但在任何时候都可用于数据通信寻址。
为啥要arp:
因为通信要mac,所以要解析ip,arp(Address Resolution Protocol)
问题:既然每个以太网设备在出厂时都有一个唯一的MAC地址了,那为什么还需要为每台主机再分配一个IP地址呢?或者说为什么每台主机都分配唯一的IP地址了,为什么还要在网络设备(如网卡,集线器,路由器等)生产时内嵌一个唯一的MAC地址呢?
解答:主要原因基于以下几点:
(1)IP地址的分配是根据网络的拓扑结构,而不是根据谁制造了网络设置。若将高效的路由选择方案建立在设备制造商的基础上而不是网络所处的拓扑位置基础上,这种方案是不可行的;
(2)当存在一个附加层的地址寻址时,设备更易于移动和维修。例如,如果一个以太网卡坏了,可以被更换,而无须取得一个新的IP地址。如果一个IP主机从一个网络移到另一个网络,可以给它一个新的IP地址,而无须换一个新的网卡;
(3)无论是局域网,还是广域网中的计算机之间的通信,最终都表现为将数据包从某种形式的链路上的初始节点出发,从一个节点传递到另一个节点,最终传送到目的节点。数据包在这些节点之间的移动都是由ARP协议负责将IP地址映射到MAC地址上来完成的。
mac是网卡的唯一标识,设备可以使用不同的ip,不过始终是一个mac
然后交换机通过转化表建立交换机端口和接入交换机的那些设备的mac之间的映射
arp是同一个网段内,把ip地址转化成mac地址的协议,arp表就是同一网段中ip地址到mac的映射,没有则进行一次arp请求,arp请求会找到同一网段中可以到达请求ip的地址的那个设备的mac(结合后面的路由表看)
然后路由表是基础网络拓扑生成的,主要是根据ip以及路由表中的内容确定下一跳是哪个设备,确定设备之后通过arp获取那个设备的mac,然后封装好就可以转发了
转发表(MAC表)、ARP表、路由表总结
地址解析协议_ARPwiki
一般是可以的,但是可能会因为struct数据设置得不好,导致受到字节对齐的影响,可能会导致计算会出问题
Socket发送和接收变长结构体
C 语言变长数组 struct 中 char data[0] 的用法
对于整型、长整型等数据类型,Big endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节);而 Little endian 则相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放据的低位字节到高位字节)。
记忆:大端_低址高字节_像左到右
例如,假设从内存地址 0x0000 开始有以下数据:
0x0000 0x0001 0x0002 0x0003
0x12 0x34 0xab 0xcd
如果我们去读取一个地址为 0x0000 的四个字节变量,若字节序为big-endian,则读出结果为0x1234abcd;若字节序为little-endian,则读出结果为0xcdab3412。
如果我们将0x1234abcd 写入到以 0x0000 开始的内存中,则Little endian 和 Big endian 模式的存放结果如下:
地址 0x0000 0x0001 0x0002 0x0003
big-endian 0x12 0x34 0xab 0xcd
little-endian 0xcd 0xab 0x34 0x12
一般来说,x86 系列 CPU 都是 little-endian 的字节序,PowerPC 通常是 big-endian,网络字节顺序也是 big-endian还有的CPU 能通过跳线来设置 CPU 工作于 Little endian 还是 Big endian 模式。
对于0x12345678的存储:
小端模式:(从低字节到高字节)
地位地址 0x78 0x56 0x34 0x12 高位地址
大端模式:(从高字节到低字节)
地位地址 0x12 0x34 0x56 0x78 高位地址
htonl() htons() 从主机字节顺序转换成网络字节顺序
ntohl() ntohs() 从网络字节顺序转换为主机字节顺序
Big-Endian转换成Little-Endian
#define BigtoLittle16(A) ((((uint16)(A) & 0xff00) >> 8) | (((uint16)(A) & 0x00ff) << 8))
#define BigtoLittle32(A) ((((uint32)(A) & 0xff000000) >> 24) | (((uint32)(A) & 0x00ff0000) >> 8) | \
(((uint32)(A) & 0x0000ff00) << 8) | (((uint32)(A) & 0x000000ff) << 24))
联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性就可以轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写。
Linux 操作系统中相关的源代码是怎么做的:
static union { char c[4]; unsigned long mylong; } endian_test = {{ 'l', '?', '?', 'b' } };
#define ENDIANNESS ((char)endian_test.mylong)
Linux 的内核作者们仅仅用一个union 变量和一个简单的宏定义就实现了一大段代码同样的功能!(如果ENDIANNESS=’l’表示系统为little endian,为’b’表示big endian)
原理:这里确定的是’l’是低字节,'b’是高字节,如果我们现在取低地址,那么如果是’b’说明 ‘低址高字节’ ,所以是大端
https://www.cnblogs.com/luxiaoxun/archive/2012/09/05/2671697.html
两个深信服cpp面试都考了这个!
#include
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(int i = int(a); i <= int(b); ++i)
#define per(i, b, a) for(int i = int(b); i >= int(a); --i)
#define mem(x, y) memset(x, y, sizeof(x))
#define SZ(x) x.size()
#define mk make_pair
#define pb push_back
#define fi first
#define se second
const ll mod=1000000007;
const int inf = 0x3f3f3f3f;
inline int rd(){char c=getchar();int x=0,f=1;while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}return x*f;}
inline ll qpow(ll a,ll b){ll ans=1%mod;for(;b;b>>=1){if(b&1)ans=ans*a%mod;a=a*a%mod;}return ans;}
class Solution {
public:
void dfs(const string& s,string cur,int i){
// sz是用掉的尺寸
// left_sz是剩下的
int sz = i <= 1 ? cur.size() : cur.size()-(i-1);
int left_sz = s.size() - sz;
if(3 == i){
// cout<< "1 here i == 3 "<
if(left_sz<=0 || left_sz > 3) return;
string sub = s.substr(sz,left_sz);
// cout<< "2 here i == 3, sub :" <
int tp = atoi(sub.c_str());
if(tp!=0 && sub[0]=='0') return;
if(tp==0 && sub.size()>1) return;
if(tp > 255) return ;
// cout<< "3 here i == 3 "<
ans.push_back(string(cur+'.'+sub));
}
else if(2 == i){
if(left_sz<2 || left_sz>6) return;
for(int k=1;k<=3;k++){
string sub = s.substr(sz,k);
// cout<< "2 here i == 2, sub :" <
int tp = atoi(sub.c_str());
if(k==3 && tp>255) return;
if(tp!=0 && sub[0]=='0') return;
if(tp==0 && sub.size()>1) return;
dfs(s,cur+'.'+sub,3);
}
}
else if(1 == i){
if(left_sz<3 || left_sz>9) return;
for(int k=1;k<=3;k++){
string sub = s.substr(sz,k);
// cout<< "2 here i == 1, sub :" <
int tp = atoi(sub.c_str());
if(k==3 && tp>255) return;
if(tp!=0 && sub[0]=='0') return;
if(tp==0 && sub.size()>1) return;
dfs(s,cur+'.'+sub,2);
}
}
else if(0 == i){
if(left_sz<4 || left_sz>12) return;
for(int k=1;k<=3;k++){
string sub = s.substr(sz,k);
// cout<< "2 here i == 0, sub :" <
int tp = atoi(sub.c_str());
if(k==3 && tp>255) return;
if(tp!=0 && sub[0]=='0') return;
if(tp==0 && sub.size()>1) return;
dfs(s,sub,1);
}
}
}
vector<string> restoreIpAddresses(string s) {
ans.clear();
dfs(s,"",0);
return ans;
}
private:
vector<string> ans;
};
class Solution_for {
public:
bool substr_ok(string sub){
int tp = atoi(sub.c_str());
int k = sub.size();
if(k==3 && tp>255) return false;
if(tp!=0 && sub[0]=='0') return false;
if(tp==0 && k>1) return false;
return true;
}
vector<string> restoreIpAddresses(string s) {
vector<string> ans;
int sz = s.size();
for(int i=1;i<4;i++){
if(i > sz) continue;
string sub1 = s.substr(0,i);
if(!substr_ok(sub1)) continue;
for(int j=1;j<4;j++){
if(i+j > sz) continue;
string sub2 = s.substr(0+i,j);
if(!substr_ok(sub2)) continue;
for(int k=1;k<4;k++){
if(i+j+k > sz) continue;
string sub3 = s.substr(i+j,k);
if(!substr_ok(sub3)) continue;
int left_sz = sz - (i+j+k);
if(left_sz<=0 || left_sz>3) continue;
// for(int p=1;p<=left_sz;p++){
string sub4 = s.substr(i+j+k,left_sz);
if(!substr_ok(sub4)) continue;
ans.push_back(sub1+'.'+sub2+'.'+sub3+'.'+sub4);
// }
}
}
}
return ans;
}
};
int main(){
Solution test;
// string s = "25525511135";
string s = "010010";
vector<string> ans;
ans = test.restoreIpAddresses(s);
for(auto x:ans) cout<<x<<endl;
return 0;
}
2020年5月30日12:07:31 今天看希尔排序,感觉和堆排序有点像
2020年5月30日15:00:17 再看又觉得不是很想,希尔排序是先把一半大的放后面,一半小的放前面,之后就是不断把范围缩小,然后向右扫描
而堆排序是先向左建堆,然后逐个抽取最大值到右边
int shellSort(int arr[], int n){
for (int gap = n/2; gap > 0; gap /= 2){
for (int i = gap; i < n; i += 1){
int temp = arr[i];
int j;
// 注意这里的j>=gap的时候还是会重复比较
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
arr[j] = arr[j - gap];
arr[j] = temp;
}
}
return 0;
}
void heapify(int arr[], int n, int i) {
// 这里对arr[]中序列化的值按照类似满二叉树的逐层读取的规则来的
int largest = i; // 将最大元素设置为堆顶元素
int l = 2 * i + 1; // left = 2*i + 1
int r = 2 * i + 2; // right = 2*i + 2
// 如果 left 比 root 大的话
if (l < n && arr[l] > arr[largest]) largest = l;
// I如果 right 比 root 大的话
if (r < n && arr[r] > arr[largest]) largest = r;
if (largest != i) {
swap(arr[i], arr[largest]);
// 递归地定义子堆,当子堆的最大被上层取走之后,那么要更新子堆
heapify(arr, n, largest);
}
}
void heapSort(int arr[], int n) {
// 建立堆
for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i);
// 一个个从堆顶取出元素
for (int i = n - 1; i >= 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
2020年5月30日16:37:35 已经看到多个人说面试官让5mins实现了
#include
#include
class String {
public:
String() : data_(new char[1]) { *data_ = '\0'; }
String(const char* str) : data_(new char[strlen(str) + 1]) {
strcpy(data_, str);
}
String(const String& rhs) : data_(new char[rhs.size() + 1]) {
strcpy(data_, rhs.c_str());
}
/* Delegate constructor in C++11
String(const String& rhs)
: String(rhs.data_)
{
}
*/
~String() { delete[] data_; }
/* Traditional:
String& operator=(const String& rhs)
{
String tmp(rhs);
swap(tmp);
return *this;
}
*/
String& operator=(String rhs) // yes, pass-by-value
{
swap(rhs);
return *this;
}
// C++ 11
// 如果是右值传递,那么就会调用这个函数
String(String&& rhs) : data_(rhs.data_) { rhs.data_ = nullptr; }
String& operator=(String&& rhs) {
swap(rhs);
return *this;
}
// Accessors
size_t size() const { return strlen(data_); }
const char* c_str() const { return data_; }
void swap(String& rhs) { std::swap(data_, rhs.data_); }
private:
char* data_;
};
void foo(String x) {}
void bar(const String& x) {}
String baz() {
String ret("world");
return ret;
}
int main() {
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
s2 = s1;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s4 = baz();
std::vector<String> svec;
svec.push_back(s0);
svec.push_back(s1);
svec.push_back(baz());
svec.push_back("good job");
}
从有序数组中选不重复的数字,作为随机数_等概率选择
#include
using namespace std;
typedef long long ll;
#define rep(i, a, b) for(int i = int(a); i <= int(b); ++i)
#define per(i, b, a) for(int i = int(b); i >= int(a); --i)
#define mem(x, y) memset(x, y, sizeof(x))
#define SZ(x) x.size()
#define mk make_pair
#define pb push_back
#define fi first
#define se second
const ll mod=1000000007;
const int inf = 0x3f3f3f3f;
inline int rd(){char c=getchar();int x=0,f=1;while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}return x*f;}
inline ll qpow(ll a,ll b){ll ans=1%mod;for(;b;b>>=1){if(b&1)ans=ans*a%mod;a=a*a%mod;}return ans;}
// 感觉第一种随机比后面的几种等概性更高,后面的好像等概性没那么高
int genknuth(int m,int n,vector<int> const & tmp){
int i;
for(i=0;i<n;i++)
// n-i == m的时候,rand()%(n-i) < m; n--,m--; 一直为true
if(rand()%(n -i) < m) {
printf("%d ",tmp[i]);
m--;
}
puts("");
return 0;
}
void gensets(int m,int n,vector<int> const & tmp) {
set<int> S;
while((int)S.size() < m)
S.insert(rand()%n);
set<int>::iterator i;
for(i = S.begin();i!=S.end();++i)
cout<<tmp[*i]<<" ";
puts("");
}
int genfloyd(int m,int n,vector<int> const & tmp){
set<int> S;
set<int>::iterator i;
for(int j = n-m; j<n;j++) {
int t = rand()%(j+1);
if(S.find(t) == S.end())
S.insert(t);
else
S.insert(j);
}
for(i=S.begin();i!=S.end();++i)
cout<<*i<<" ";
puts("");
return 0;
}
int genshuf(int m,int n){
int i,j;
int *x = new int[n];
for(i = 0;i<n;i++)
x[i] = i;
for(i = 0;i<m;i++) {
// j = randint(i,n-1);
j = rand()%(n-1 - i) + i;
//randint产生i到n-1之间的随机数
int t = x[i];x[i] = x[j];x[j] = t;
}
//sort(x,x+m);
//sort是为了按序输出
for(i=0;i<m;i++)
cout<<x[i]<<"\n";
return 0;
}
/*
给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,
请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出m个不重复的数据。
这里的不重复是下标不重复
算法思路大致如下:
1. 如果接收的数据量小于m,则依次放入蓄水池。
2. 当接收到第i个数据时,i >= m,在[0, i]范围内取以随机数d,若d的落在[0, m-1]范围内,则用接收到的第i个数据替换蓄水池中的第d个数据。
重复步骤2。
3. 算法的精妙之处在于:当处理完所有的数据时,蓄水池中的每个数据都是以m/N的概率获得的。
下面用白话文推导验证该算法。假设数据开始编号为1.
第i个接收到的数据最后能够留在蓄水池中的概率=第i个数据进入过蓄水池的概率*之后第i个数据不被替换的概率(第i+1到第N次处理数据都不会被替换)。
1. 当i<=m时,数据直接放进蓄水池,所以第i个数据进入过蓄水池的概率=1。
2. 当i>m时,在[1,i]内选取随机数d,如果d<=m,则使用第i个数据替换蓄水池中第d个数据,因此第i个数据进入过蓄水池的概率=m/i。
3. 当i<=m时,程序从接收到第m+1个数据时开始执行替换操作,第m+1次处理会替换池中数据的为m/(m+1),会替换掉第i个数据的概率为1/m,则第m+1次处理替换掉第i个数据的概率为(m/(m+1))*(1/m)=1/(m+1),不被替换的概率为1-1/(m+1)=m/(m+1)。依次,第m+2次处理不替换掉第i个数据概率为(m+1)/(m+2)...第N次处理不替换掉第i个数据的概率为(N-1)/N。所以,之后第i个数据不被替换的概率=m/(m+1)*(m+1)/(m+2)*...*(N-1)/N=m/N。
4. 当i>m时,程序从接收到第i+1个数据时开始有可能替换第i个数据。则参考上述第3点,之后第i个数据不被替换的概率=i/N。
5. 结合第1点和第3点可知,当i<=m时,第i个接收到的数据最后留在蓄水池中的概率=1*m/N=m/N。结合第2点和第4点可知,当i>m时,第i个接收到的数据留在蓄水池中的概率=m/i*i/N=m/N。综上可知,每个数据最后被选中留在蓄水池中的概率为m/N。
这个算法建立在统计学基础上,很巧妙地获得了“m/N”这个概率。
分布式-几核分几个池子, 见 https://www.jianshu.com/p/7a9ea6ece2af
*/
// tmp可以是数据流,因为这是蓄水池算法..
void Reservoir_Sampling(const int m,vector<int> const & tmp){
const int n = tmp.size();
int reservoir[m];
for(int i=0;i<m;i++) reservoir[i] = tmp[i];
for(int i=m;i<n;i++){
int tmp_id = rand()%(i+1);
if(tmp_id < m)
reservoir[tmp_id] = tmp[i];
}
for(auto x:reservoir) printf("%d ", x);
puts("");
}
int main(){
int seed=time(NULL);
srand(seed);
vector<int> tmp;
tmp.pb(2);
tmp.pb(7);
tmp.pb(9);
tmp.pb(15);
tmp.pb(88);
genknuth(2,5,tmp);
genknuth(4,5,tmp);
genknuth(4,5,tmp);
genknuth(4,5,tmp);
genknuth(4,5,tmp);
genknuth(4,5,tmp);
genknuth(4,5,tmp);
gensets(2,5,tmp);
// 测试genfloyd下标有问题...
genfloyd(2,5,tmp);
Reservoir_Sampling(2,tmp);
return 0;
}
等概率无重复的从n个数中选取m个数
蓄水池抽样算法(Reservoir Sampling)
/*
具体见:
https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91
感觉源码的插入有问题,自己改了一下
然后删除中情形二的代码也有问题,wiki源码自己注释反了___版本匹配错了
// ```cpp
if(p == p->parent->leftTree)
//rotate_left(p->sibling());
rotate_left(p->parent);
// ```
应该要这样
// ```cpp
if(p == p->parent->leftTree)
rotate_left(p->sibling());
//rotate_left(p->parent);
// ```
************************
这个版本的左右旋源码核心是使用的那个节点的值是3个点值中的中间大小
************************
然后就是在删除那里卡死了,去看 STL源码剖析 ,发现那本书直接没有讲删除
然后找到一篇知乎文章,讲得比较妙的 B ... 让我瞬间懂了为啥要delete_case
下面我们开始讨论修复操作(下面的叶子节点都是指非NULL的叶子节点):
A. 删除的是叶子节点且该叶子节点是红色的 ---> 无需修复,因为它不会破坏红黑树的5个特性
B. 删除的是叶子节点且该叶子节点是黑色的 ---> 很明显会破坏特性5,需要修复。 ___ 对于其他路的影响
C. 删除的节点(为了便于叙述我们将其称为P)下面有一个子节点 S,对于这种情况我们通过 将P和S的值交换的方式,
巧妙的将删除P变为删除S,S是叶子节点,这样C这种情况就会转 换为A, B这两种情况:
C1: P为黑色,S为红色 ---> 对应 A 这种情况
C2: P为黑色或红色,S为黑色 --- > 对应 B 这种情况
D. 删除的节点有两个子节点,对于这种情况,我们通过将P和它的后继节点N的值交换的方式,
将删除节点P转换为删除后继节点N,而后继节点只可能是以下两种情况:
D1: N是叶子节点 --- > 对应情况 A 或 B
D2: N有一个子节点 ---- > 对应情况 C
所以通过上面的分析我们发现,红黑树节点删除后的修复操作都可以转换为 A 或 B这两种情况,而A不需要修复,
所以我们只需要研究B这种情况如何修复就行了。
*/
#define BLACK 1
#define RED 0
#include
using namespace std;
class bst {
private:
struct Node {
int value;
bool color;
Node *leftTree, *rightTree, *parent;
Node() : value(0), color(RED), leftTree(NULL), rightTree(NULL), parent(NULL) { }
Node* grandparent() {
if(parent == NULL){
return NULL;
}
return parent->parent;
}
Node* uncle() {
if(grandparent() == NULL) {
return NULL;
}
if(parent == grandparent()->rightTree)
return grandparent()->leftTree;
else
return grandparent()->rightTree;
}
Node* sibling() {
if(parent->leftTree == this)
return parent->rightTree;
else
return parent->leftTree;
}
};
/*
左右旋核心,掌握5个点的父子关系,分别是y,p,fa,root,gp
先是处理y和fa的父子关系
然后再是p和fa的父子关系
再是p和root的关系
最后是gp和p的父子关系
*/
// 这里的p其实是"右旋"解析图中的 P
// 这个版本的右旋是p作为左儿子的右旋
// 三点中的中值
/*
G(maybe NULL)
/
P
/
N
旋转后:
P
/ \
N G
*/
void rotate_right(Node *p){
Node *gp = p->grandparent();
Node *fa = p->parent;
Node *y = p->rightTree;
fa->leftTree = y;
if(y != NIL)
y->parent = fa;
p->rightTree = fa;
fa->parent = p;
if(root == fa)
root = p;
p->parent = gp;
if(gp != NULL){
if(gp->leftTree == fa)
gp->leftTree = p;
else
gp->rightTree = p;
}
}
// 这里的p其实是"左旋"解析图中的 N
// 这个版本的左旋是p作为父节点的右儿子的左旋
// 三点中的中值
/*
G(maybe NULL)
/
P
\
N
转后:
G
/
N
/
P
*/
void rotate_left(Node *p){
if(p->parent == NULL){
root = p;
return;
}
// gp可能为nullptr的
// 一开始就把很多节点都存起来了,所以超级安全
Node *gp = p->grandparent();
Node *fa = p->parent;
Node *y = p->leftTree;
fa->rightTree = y;
if(y != NIL)
y->parent = fa;
p->leftTree = fa;
fa->parent = p;
if(root == fa)
root = p;
p->parent = gp;
if(gp != NULL){
if(gp->leftTree == fa)
gp->leftTree = p;
else
gp->rightTree = p;
}
}
void inorder(Node *p){
if(p == NIL)
return;
if(p->leftTree)
inorder(p->leftTree);
cout << p->value << " ";
if(p->rightTree)
inorder(p->rightTree);
}
string outputColor (bool color) {
return color ? "BLACK" : "RED";
}
Node* getSmallestChild(Node *p){
if(p->leftTree == NIL)
return p;
return getSmallestChild(p->leftTree);
}
bool delete_child(Node *p, int data){
if(p->value > data){
if(p->leftTree == NIL){
return false;
}
return delete_child(p->leftTree, data);
} else if(p->value < data){
if(p->rightTree == NIL){
return false;
}
return delete_child(p->rightTree, data);
} else if(p->value == data){
if(p->rightTree == NIL){
// 右孩子为NIL,就在左子树上做文章
delete_one_child (p);
return true;
}
// 右孩子不为NIL,就获取右子树中最小的那个节点
// 那个节点要么左孩子为NIL,要么两边都为NIL
// 综合前面的情况可以知道,两个孩子都可能为NIL or 其中的一个
Node *smallest = getSmallestChild(p->rightTree);
swap(p->value, smallest->value);
delete_one_child (smallest);
return true;
}else{
return false;
}
}
void delete_one_child(Node *p){
Node *child = p->leftTree == NIL ? p->rightTree : p->leftTree;
if(p->parent == NULL && p->leftTree == NIL && p->rightTree == NIL){
p = NULL;
root = p;
return;
}
if(p->parent == NULL){
delete p;
child->parent = NULL;
root = child;
root->color = BLACK;
return;
}
if(p->parent->leftTree == p){
p->parent->leftTree = child;
} else {
p->parent->rightTree = child;
}
child->parent = p->parent;
if(p->color == BLACK){
if(child->color == RED){
child->color = BLACK;
} else
// 儿子和被删除的父亲节点都是黑色
// 两个孩子都必须为叶子
// 那还要这个delete_case干嘛??
// 卡了几个小时
// 傻逼了,所以就是这一条路的黑色节点-1!!!会对其他路影响
// 原来要删除的p被删除了,用它两个NIL叶子中的一个顶替了它的位置!
// 所以之后讨论它的兄弟,其实是原来要删除的p的兄弟
// NIL顶替位置
delete_case (child);
}
delete p;
}
void delete_case(Node *p){
if(p->parent == NULL){
p->color = BLACK;
return;
}
// 情形二
// 原来的位置的兄弟为RED,则兄弟存在,不为NIL
if(p->sibling()->color == RED) {
p->parent->color = RED;
p->sibling()->color = BLACK;
if(p == p->parent->leftTree)
rotate_left(p->sibling());
// rotate_left(p->parent);
else
//rotate_right(p->sibling());
rotate_right(p->parent);
}
// 情形3
if(p->parent->color == BLACK && p->sibling()->color == BLACK
&& p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK) {
p->sibling()->color = RED;
delete_case(p->parent);
} else if(p->parent->color == RED && p->sibling()->color == BLACK
&& p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK) {
// 情形4
p->sibling()->color = RED;
p->parent->color = BLACK;
} else {
if(p->sibling()->color == BLACK) {
// 情形5__算是一个中间处理
if(p == p->parent->leftTree && p->sibling()->leftTree->color == RED
&& p->sibling()->rightTree->color == BLACK) {
p->sibling()->color = RED;
p->sibling()->leftTree->color = BLACK;
rotate_right(p->sibling()->leftTree);
} else if(p == p->parent->rightTree && p->sibling()->leftTree->color == BLACK
&& p->sibling()->rightTree->color == RED) {
p->sibling()->color = RED;
p->sibling()->rightTree->color = BLACK;
rotate_left(p->sibling()->rightTree);
}
}
// 情形六
// 下面两行的颜色操作根据wiki来说是 没有意义的,
// 本来就是相同的黑色__因为兄弟颜色是红的在 情形二 就处理过了
// 而且父亲是红也在情形4和5处理过了__不对__情形4和5没处理完
// 见图片 ___ wiki中没讲的情形7_代码兼顾了
p->sibling()->color = p->parent->color;
p->parent->color = BLACK;
if(p == p->parent->leftTree){
p->sibling()->rightTree->color = BLACK;
rotate_left(p->sibling());
} else {
p->sibling()->leftTree->color = BLACK;
rotate_right(p->sibling());
}
}
}
void insert(Node *p, int data){
if(p->value >= data){
if(p->leftTree != NIL)
insert(p->leftTree, data);
else {
Node *tmp = new Node();
tmp->value = data;
tmp->leftTree = tmp->rightTree = NIL;
tmp->parent = p;
p->leftTree = tmp;
insert_case (tmp);
}
} else {
if(p->rightTree != NIL)
insert(p->rightTree, data);
else {
Node *tmp = new Node();
tmp->value = data;
tmp->leftTree = tmp->rightTree = NIL;
tmp->parent = p;
p->rightTree = tmp;
insert_case (tmp);
}
}
}
void insert_case(Node *p){
/*
情形1:新节点N位于树的根上,没有父节点。
在这种情形下,我们把它重绘为黑色以满足性质2。
因为它在每个路径上对黑节点数目增加一,性质5符合。
*/
if(p->parent == NULL){
root = p;
p->color = BLACK;
return;
}
/*
情形2(其实是下面if的else,不用写):新节点的父节点P是黑色,所以性质4没有失效
(新节点是红色的)。在这种情形下,树仍是有效的。
性质5也未受到威胁,尽管新节点N有两个黑色叶子子节点;
但由于新节点N是红色,通过它的每个子节点的路径就都有同
通过它所取代的黑色的叶子的路径同样数目的黑色节点,
所以依然满足这个性质。
*/
if(p->parent->color == RED){
// 情形3
if(p->uncle()->color == RED) {
p->parent->color = p->uncle()->color = BLACK;
p->grandparent()->color = RED;
// 相当于在grandparent插入了一个红色节点
insert_case(p->grandparent());
} else {
if(p->parent->rightTree == p && p->grandparent()->leftTree == p->parent) {
rotate_left(p);
p = p->leftTree;
goto left_left;
// 2020年6月3日17:43:52 感觉下面两句都有问题,改改
// p->color = BLACK;
// // 下面这一句有问题吧,万一其中一个是NIL...呢?? 怎么能标红?
// p->leftTree->color = p->rightTree->color = RED;
} else if(p->parent->leftTree == p && p->grandparent()->rightTree == p->parent) {
rotate_right(p);
p = p->rightTree;
goto right_right;
// p->color = BLACK;
// p->leftTree->color = p->rightTree->color = RED;
} else if(p->parent->leftTree == p && p->grandparent()->leftTree == p->parent) {
left_left:
p->parent->color = BLACK;
p->grandparent()->color = RED;
rotate_right(p->parent);
} else if(p->parent->rightTree == p && p->grandparent()->rightTree == p->parent) {
right_right:
p->parent->color = BLACK;
p->grandparent()->color = RED;
rotate_left(p->parent);
}
}
}
}
void DeleteTree(Node *p){
if(!p || p == NIL){
return;
}
DeleteTree(p->leftTree);
DeleteTree(p->rightTree);
delete p;
}
public:
bst() {
NIL = new Node();
NIL->color = BLACK;
root = NULL;
}
~bst() {
if (root)
DeleteTree (root);
delete NIL;
}
void inorder() {
if(root == NULL)
return;
inorder (root);
cout << endl;
}
void insert (int x) {
if(root == NULL){
root = new Node();
root->color = BLACK;
root->leftTree = root->rightTree = NIL;
root->value = x;
} else {
insert(root, x);
}
}
bool delete_value (int data) {
return delete_child(root, data);
}
private:
Node *root, *NIL;
};
int main(int argc, char const *argv[])
{
bst rb_tree;
return 0;
}
wiki红黑树
红黑树的一些面试题