11169 字
56 分钟
C++基础知识
2023-03-19

C++基础知识点#

引用#

记忆

概念 常引用 下面3条

给变量起别名,没有常引用

  1. 引用变量创建必须初始化
  2. 引用变量不能赋值为空,必须指向一个已定义的对象
  3. 引用变量不可以改变指向

参数默认值#

记忆

概念 定义默认值顺序 缺省顺序

参数形参处 可以定义缺省的参数值,当调用时不传递该位置的参数 就相当于 使用该默认值。参数默认值只能从后开始依次定义,不能有跨越的不定义默认值

void run(int a, int b, int c = 10, int d = 20) {
}
//void run2(int a, int b=10, int c, int d=21, int f) {
//}错误的写法,参数的默认值只能从后往前依次定义

#

记忆点

类有 访问权限 相关

类里面有成员变量和成员方法(函数)

在内存中占有空间的就叫对象

类的默认访问权限是私有的 private只可以在类内访问

class people {
private:
int age = 1;
int weight = 100;
public://公有的 外界可以访问,可以通过对象访问
people (int age) {
this->age = age;
weight = 3;
cout << "调用了构造函数";
}
void fun() {
cout << "我吃饭了" << endl;
}
void getAge() {
cout << age << endl;
}
}
int main() {
//malloc 在堆区申请空间
//new首先在堆区创建了一块空间 然后会调用该对象的构造函数
people *p = new people(2);//在堆区创建了一个sizeof(people)大小的空间
//然后调用构造函数 类的大小只算成员变量和结构体对齐相同
people *q = new people(3);
p->getAge();
return 0;
}

?this指针#

概念

成员函数中,表示使用当前行为的对象就是this this是一个当前类类型的地址,指向了当前对象

构造函数#

记忆

样 返回值 如果没实现/自己实现了构造函数

  • 与类同名
  • 没有返回值
  • 如果没有实现构造函数,编译器会有一个默认的无参构造函数
  • 如果自己实现了构造函数,编译器将不会生成默认构造函数

访问修饰符#

记忆

各权限

private 只有当前类内才有权限访问这个成员 扩展友元函数

protect 本类和本类的子类

public 所有位置

成员的默认访问修饰符为private 扩展struct

?友元函数#

记忆

概念 特点

类中可以声明一个友元类或友元函数,让这个友元类或友元函数可以访问本类的私有或受保护成员

特点:单向,不可传递,不可继承

class Obj {
int mem;//私有成员
friend class A;//声明了Obj类的一个友元类
friend void run();//声明了Obj类的一个友元全局函数
};
class A {
void func() {
Obj o;
o.mem;//可以使用私有成员
}
};
void run() {
Obj o;
o.mem;
}

创建对象#

记忆

一共有三种方式 概念分别是

创建栈区对象#

用类似定义变量的语法创建存在栈区的对象,出了作用域会自动析构,生命由系统自动管理

void run() {
A a;
//A a2();这么写符合声明函数的语法,声明了一个:返回值是A类型,函数名是a2,参数列表是无参的
A a3(25, 2);
}

匿名构造对象#

创建一个没有名字的临时对象,当本行代码结束时,对象被系统销毁

void run() {
A();//创建匿名对象
A(2, 3);//用参数来创建匿名对象
}

在堆区创建对象#

堆区创建对象,对象的销毁由程序员控制,大的对象栈区放不下就必须在堆区创建

void run() {
A* p = new A();
A* p2 = new A(int, int);
A* p3 = new A[20];//创建数组
delete p;
delete p2;
delete[] p3;//析构数组时,不delete会报错,准确来讲是有析构函数的数组必须delete[]
//总之,数组delete[]一定不会错
}

二维数组#

样 定义方法

定义

vector<vector<int>> vec = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

操作

int row = vec.size();//行,一个一维数组占一行
int col = vec[0].size();//列,第一个一维数组的元素个数
vec[0].push_back(11);//向第0行末尾加一个元素
vector<int> p = {6, 6, 6};
vec.push_back(p);//向二维数组中加入一行
//输出
for (int i = 0; i < vec.size(); ++i) {
for (int j = 0; i < vec[i].size(); ++j) {
cout << vec[i][j] << " ";
}
cout << endl;
}

实际上C++中没有二维数组,一维数组中放的一维数组就可以看成是二维数组,二维数组的行,看成一维数组中有几个元素(一维数组)

构造函数与析构函数#

构造析构语法 重载 自动手动

注意:C++的一切操作都是在内存上的操作

C++利用构造函数和析构函数实现了对象所占内存的初始化和清空操作,对象的初始化操作和清除操作时强制执行构造函数和析构函数,如果我们没有自己实现构造函数和析构函数,编译器将会提供默认的构造函数和析构函数

  • 构造函数是作用于对象初始化的,主要作用是在创建对象时为对象的成员变量赋值,构造函数由编译器自动调用,不需要手动调用。当我们使用 new 时,会先使用 malloc 申请一块堆区空间,然后调用构造函数为申请的空间赋值
  • 析构函数作用于对象销毁工作,清空对象所占有的堆区内存

构造函数语法#

  • 没有返回值:类名 () {}
  • 函数名和类名相同前面什么也不写
  • 构造函数可以有参数,可以重载
  • 创建对象时编译器会自动调用构造函数,不需要手动调用
  • 如果实现了有参数的构造函数编译器不会实现默认的构造函数

析构函数语法#

  • 没有返回值:~类名 () {}
  • 函数名称与类名相同但是前面要有~
  • 析构函数不可以有参数,因此不可以发生重载
  • 编译器在对象销毁前会自动调用析构函数,不需要手动调用
class people {
private:
int weight;
int height;
int age;
string name;
string bloodType;
public:
//构造函数
people() {
cout << "调用了构造函数" << endl;
}
people(int weight) {
this->weight = weight;
cout << "体重为:" << endl;
}
people(int weight, int height, int age, string name, string bloodType) {
this->weight = weight;
this->height = height;
this->age = age;
this->name = name;
this->bloodType = bloodType;
cout << "姓名为:" << name << endl;
cout << "体重为:" << weight << endl;
cout << "年龄为:" << age << endl;
}
~people() {
cout << "调用了析构函数" << endl;
}
};
int main() {
//自定义类型只能用new不能用malloc
//因为new会申请空间并且走构造函数,malloc只会申请空间
people p(1, 2, 3, "asd", "A");//在栈区创建的对象,编译器会自动释放
people *p1 = new people(99999);//在堆区创建的对象,需要手动释放,new会走malloc和构造函数
delete p1;//delete和new是搭配的 delete会走析构函数和free
return 0;
}

初始化参数列表#

使用 必须用的 顺序

只能在构造函数里使用该语法,可以给所有成员设置初始化参数,const类型和引用类型必须在初始化参数列表中初始化,成员的构造顺序和在初始化参数列表中的顺序无关,与在类中的声明顺序有关

拷贝构造函数#

概念 关联 区分 使用 深浅 区别 优缺点 什么时候用

拷贝构造函数是把对象以值的形式当作参数传入,利用传入的对象生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例,拷贝构造函数的功能是使用对象创建一个对象实例,也就是说使用对象初始化另一个新的对象,赋值运算符是将一个对象的值赋值给另一个已经存在的对象,调用的是拷贝构造函数还是赋值运算符,主要看是否有新的对象产生

使用场景

  • 对象作为函数的返回值以值的方式从函数返回
  • 对象作为函数参数,以值传递的方式穿给参数
  • 使用一个对象给另一个对象初始化
class A {
private:
int n = 0;
int *p = nullptr;//NULL 宏定义 0 nullptr 是空指针 void*
public:
A(int _size) {
if (_size > 0) {
n = _size;
p = (int*)malloc(sizeof(int)* n);
}
}
//如果不加引用在通过实参创建形参的过程中,还会调用拷贝构造函数
//还会有实参创建形参的过程导致无限循环该过程
A (const A &other) {
this->n = other.n;
this->p = other.p;
cout << "调用了拷贝构造函数" << endl;
}
};
int main() {
A a(1);
A b(a);
A c(3);
c = b;//会走赋值运算符,把对象赋值给一个已经存在的实例
cout << "-----------------" << endl;
A d = c;//用对象初始化对象会走拷贝构造函数
return 0;
}
深拷贝构造函数#

对象中含有指针类型的成员变量时需要用深拷贝构造,否则用浅拷贝构造

编译器默认的拷贝构造函数是浅拷贝构造函数

如果对象中含有指针变量用了浅拷贝构造,那么会导致两个指针变量指向同一块地址空间,那么在对象释放时会导致一块空间释放两次

浅拷贝和深拷贝的区别在于两个指针变量指向的是一块空间还是指向不同的空间。假设没有创建内存的操作就是浅拷贝,否则是深拷贝

class A {
private:
int n = 0;
int *p = nullptr;//NULL宏定义0 nullptr是空指针void*
public:
A (int _size) {
if (_size > 0) {
n = _size;
p = (int *)malloc(sizeof(int) *n);
}
}
//如果不加引用在通过实参创建形参过程中,还会调用拷贝构造函数,还会有实参创建形参的过程导致无限循环该过程
A (const A &other) {//深拷贝
this->n = other.n;
p = (int *)malloc(sizeof(int)* n);
cout << "调用了拷贝构造函数" << endl;
}
~A () {
if (p != nullptr) {
free(p);
cout << "调用了析构函数释放了堆区空间" << endl;
}
}
};
int main() {
A a(1);
A b(a);
return 0;
}
浅拷贝构造函数#

对象中含有指针类型的成员变量时需要用深拷贝构造,如果对象里面有指针变量,两个对象里面的指针变量指向同一块地址,在释放内存时会报错

class A {
private:
int n = 0;
int *p = nullptr;//NULL宏定义0 nullptr是空指针void*
public:
A (int _size) {
if (_size > 0) {
n = _size;
p = (int *)malloc(sizeof(int) *n);
}
}
A (const A &other) {//浅拷贝
this->n = other.n;
this->p = other.p;
cout << "调用了拷贝构造函数" << endl;
}
~A () {
if (p != nullptr) {
free(p);
cout << "调用了析构函数释放了堆区空间" << endl;
}
}
};
int main() {
A a(1);
A b(a);
return 0;
}

NULL和nullptr#

NULL宏定义0 nullptr是空指针void*

静态成员#

包括 样 用 初始化 访问

类可以拥有静态成员,包括静态成员函数和静态成员变量

静态成员就类似于全局变量或全局函数,并且拥有访问修饰符,使用静态需要加类名作用域

静态成员变量必须类外初始化

相比之下静态成员函数没有什么限制 静态成员变量和静态成员函数不属于某个对象,属于类,可以直接通过类名加 :: 调用

静态成员函数只允许访问静态成员变量

class people {
public:
static int a;
int b;
static void fun() {
a = 2;//静态成员函数只允许访问静态成员变量
}
};
int people::a = 1;//静态成员变量必须类外初始化
int main() {
people::a;
people::fun();
return 0;
}

继承性#

构造析构顺序 父类没无参构造

创建对象时,每个类都会先调用父类的构造,当父类构造完成后再构造子类

析构对象时,每个类都会先析构当前类,当前类析构函数调用完后再调用父类析构函数

如果父类没有无参构造,就需要手动指定调用构造传参数(使用初始化参数列表传参)

class father {
public:
int a;
father(int a) {
cout << "父类构造函数" << endl;
}
father(int a, int b) {
cout << "父类构造函数" << endl;
}
void fun() {
cout << "father fun" << endl;
}
~father() {
cout << "父类对象析构" << endl;
}
};
class child:public father {
public:
int b;
child():father(1) {
cout << "子类构造函数" << endl;
}
~child() {
cout << "子类对象析构" << endl;
}
};

多继承#

概念 关联

一个类有多个父类,不推荐如此使用,会导致难以控制,比如java或C#等其他面下那个语言中都不支持多继承

菱形继承#

会导致D中有两份A的数据,多数情况下这都是多余的,并且可能导致二义性错误

虚继承#

概念

解决菱形继承中,父类被继承多次的问题,使用虚继承可以解决,虚继承时只会继承到一份

class grandFather {
public:
int a;
};
class father:virtual public grandFather {
public:
};
class mather:virtual public grandFather {
};
class child:public father, public mather {
public:
};

多态性#

记忆

静态多态 动态多态 概念 虚函数

动态多态:重写:父类指针指向子类对象,父类指针动态的调用子类对象中的重写函数

class father {
public:
virtual void fun() {//虚函数
}
father() {
cout << "father的构造函数" << endl;
}
~father() {
cout << "father的析构函数" << endl;
}
};
class child:public father {
child() {
cout << "child的构造函数" << endl;
}
~child() {
cout << "child的析构函数" << endl;
}
};
int main() {
father *f = new child();
delete f;
return 0;
}

虚函数#

关联

C++默认的成员函数不开启多态,只有将函数声明为virtual函数,该函数才可以被重写

class father {
public:
virtual void fun() {//虚函数
}
}
虚析构#

概念 为什么

析构函数应该设置为虚函数,不然可能内存泄漏

当类之间具有继承关系时,在多态中应该将析构函数设置为虚函数,让析构函数具有多态,此时父类指针delete时才会准确的调用到子类的析构函数

class father {
public:
virtual void fun() {//虚函数
}
father() {
cout << "father的构造函数" << endl;
}
~father() {
cout << "father的析构函数" << endl;
}
};
class child:public father {
public:
child() {
cout << "child的构造函数" << endl;
}
~child() {
cout << "child的析构函数" << endl;
}
}
纯虚函数#

概念 纯虚类 抽象类

如果父类中的一个虚函数其自身的实现无意义,此时可以将该虚函数定义为纯虚函数,拥有至少一个纯虚函数的类是纯虚类(抽象类),纯虚类不能直接创建对象,子类只有重写了所有纯虚函数后才能创建子类对象

class father {//抽象类(纯虚类)
public:
virtual void fun() = 0;//纯虚函数
father() {
cout << "father的构造函数" << endl;
}
~father() {
cout << "father的析构函数" << endl;
}
};
class child:public father {
public:
child() {
cout << "child的构造函数" << endl;
}
void fun() {
cout << "子类的fun" endl;
}
~child() {
cout << "child的析构函数" << endl;
}
};

?覆盖(重写)#

概念

在父类和子类中返回值类型相同,函数名相同,参数类型相同的虚函数是覆盖,具体调用哪个函数看指针指向哪个类的对象

?隐藏#

概念

在父类和子类中只有函数名相同就会发生隐藏,不要求返回值和参数相同,具体执行父类函数还是子类函数看是哪个类的对象指针调用的这个函数

//多态:父类指针指向子类对象:父类指针动态的调用子类对象里面的重写函数
class duck {//纯虚类(抽象类) 作为基类
string country;
string color;
int weight;
public:
virtual void Swimming() = 0;
void Lay_eggs() {
cout << "下蛋嘿嘿嘿" << endl;
}
};
class psyduck:public duck {//可达鸭
public:
virtual void Swimming() {
cout << "可达鸭会玩铁男但是不会游泳" << endl;
};
void Lay_eggs() {
cout << "可达鸭下单嘿嘿嘿" << endl;
}
};
class ugly_duckling:public duck {//丑小鸭
public:
virtual void Swimming() {
cout << "会游泳但是不会打联盟" << endl;
};
};
class Roasted_Duck:public duck {//烤鸭
public:
virtual void Swimming() {
cout << "烤鸭不会游泳,但是香" << endl;
};
};
int main() {
//通过父类指针指向不同的子类对象,父类指针动态的去调用子类对象中重写的函数
ugly_duckling *d = new ugly_duckling();
d->Swimming();
d->Lay_eggs();
cout << endl;
Roasted_Duck *d1 = new Roasted_Duck();
d1->Swimming();
d1->Lay_eggs();
cout << endl;
psyduck *d2 = new psyduck();
d2->Swimming();
d2->Lay_eggs();
//函数隐藏,子类中只需要函数名和父类中的函数名相同就会发生函数隐藏
//函数隐藏具体执行父类中的同名函数还是子类中的同名函数,看调用这个函数的类型
return 0;
}

优先级队列#

概念#

样 定义方法 底层

优先级队列,顾名思义,里面元素是有优先级的,自带排序功能

priority_queue 又称为优先队列,其底层是用来进行实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。

当然,可以在任何时候往优先队列里面加入元素,而优先队列底层的数据结构堆(heap)会随时调整结构,使得每次的队首元素都是优先级最高的

和队列不同的是优先队列没有 front()函数与back()函数,而只能通过 top() 函数来访问队首元素(也称堆顶元素),也就是优先级最高的元素。

定义方法#

priority_queue<数据类型> 名称;

例如:

priority_queue<int> que;

常用函数#

#include<queue>//使用前要先添加头文件
que.push(i);//把i放进优先级队列里
que.pop();//弹出堆顶元素
que.top();//获取堆顶元素但不弹出
que.size();//优先级队列中元素个数
que.empty();//判断优先级队列是否为空,如果为空返回1,不为空返回0

元素优先级设置#

优先队列最为强大的是它的排序功能,如何定义队列的优先级是运用好优先队列的关键,下面分别介绍基本数据类型(例如:int char double)与结构体类型的优先级设置的方法。

1)基本数据类型的优先级设置#

最大/小堆样 声明 头文件

(即可以直接使用的数据类型),优先队列对他们的优先级设置一般是数字越大的优先级越高,因此队首是优先级最大的那个(如果是 char 类型,则是字典序最大的)。以 int 型为例下面两种是等价的:

priority_queue<int>q;//默认最大堆 即堆顶元素最大priority_queue<int,vector<int>,less<int> >q;//最大堆

可以发现第二种定义方式的尖括号内多出了两个参数:其中 vector填写的是承载底层数据结构堆 (heap)的容器,如果是其他类型 可写为 vector或vector;

第三个参数 less 则是对第一个参数的比较类,!!less 表示数字大的优先级越大(降序),而 greater 表示数字小的优先级越大(升序)。

#include<iostream>
#include<queue>
using namespace std;
int main()
{
priority_queue<int,vector<int>,greater<int> > q;
q.push(3); //入队
q.push(5);
q.push(1);
cout<<q.top()<<endl; //获取队首元素
return 0;
}
2)结构体的优先级设置#
// 使用friend其排序与sort排序刚好相反
struct node
{
string name;
int price;
//friend bool operator<(const node &f1,const node &f2)
//建议使用上面的引用来提高效率
friend bool operator<(node f1,node f2)
{
return f1.price<f2.price;
}
};
priority_queue<node> q;
// 不使用 friend 友元将它写在外面
struct node
{
string name;
int price;
};
struct cmp
{
bool operator() (node f1,node f2)
{
return f1.price>f2.price;
}
}
priority_queue<node,vector<node>,cmp>;

首先对某一对象建立结构体,现在希望价格高的优先级高,就需要重载(overload)小于号“<”.重载是指对已有的运算符进行重新定义。

可以看到,node 结构体中增加了一个函数,其中“friend”为友元,后面的”bool operator<(node f1,node f2) ” 这里是对小于号进行了重载事实上也仅仅只能对小于号进行重载,重载大于号会编译错误,因为从数学上来讲只需要重载小于号即 f1>f2<==>f2<f1,而我们发现 return f1.price<f2.price ,重载后小于号还是小于号的作用,因此就可以直接定义 node 类型的优先队列,其内部就是以价格高的水果为优先级高,因此可以直接定义为:

priority_queue q;

priority_queue<int, vector<int>, greater<int>> que;//最小堆
priority_queue<int> q;//默认优先级队列
for (int i = 0; i < 5; ++i) {
que.push(i);
}
que.pop();//弹出堆顶元素
cout << que.top() << endl;//获取堆顶元素但不弹出
que.size();//优先级队列中元素个数
que.empty();//判断优先级队列是否为空,如果为空返回1 不为空返回0
priority_queue<int, vector<int>, less<int>> que2;//最大堆
que.push(6);
que.push(1);
que.push(8);
que.push(2);
que.push(7);
cout << que.size() << endl;
cout << "堆顶元素" << que.top() << endl;
que.pop();
cout << "移除堆顶元素后" << endl;
cout << que.top << endl;
cout << "优先级队列是否为空" << que.empty() << endl;

priority_queue的常见用途#

priority_queue 可以解决一些贪心问题,也可以对 Dijkstra 算法进行优化。

注意#

top empty

需要要注意使用 top() 函数之前,必须用 empty() 判断优先队列是否为空。

栈和队列#

栈定义方法及常用函数#

stack<int> s;//定义 stack<数据类型> 名称;
s.push();//入栈 将元素放到栈顶
s.top();//获取栈顶元素
s.pop();//弹出栈顶元素
s.empty();//判断栈是否为空 为空返回1 不为空返回0
s.size();//返回栈中元素个数

队列定义方法及常用函数#

queue<string> q;//定义 queue<数据类型> 名称;
q.push("Hello, World!");//队列先进先出 所以push是向队尾放元素
q.pop();//弹出队首元素 即最前面那个
q.size();//返回队列元素个数 返回类型为unsigned int
q.empty();//判断是否为空 空返回1 不空返回0
q.front();//返回队首元素
q.back();//返回队尾元素
#include<stack>//栈 先进后出
#include<queue>//队列 先进先出
int main() {
stack<int> stk;//栈里面放的元素类型是int
stk.push(1);
stk.push(2);
stk.push(3);
stk.push(4);
stk.push(5);
while (!stk.empty()) {
cout << "栈顶元素" << stk.top() << endl;
stk.pop();
}
queue<int> que;
for (int i = 0; i < 4; ++i) {
que.push(i);
}
while (!que.empty()) {
cout << "队列的首元素:" << que.front() << "队列的尾元素:" << que.back() << endl;
que.pop();
}
return 0;
}

链表#

它是动态的进行存储分配的一种结构,链表和数组比较,不用事先确定存储空间,而是根据需要开辟内存单元

链表分为数据域和指针域

  • 数据域:存储数据,例如一个人的信息:姓名 年龄 班级
  • 指针域:是一个指针变量,存储下一个节点的地址,使本来不连续的空间通过该指针链接到一起

代码#

List.h文件#
#pragma once
//.h文件声明类中的函数,结构体变量等
#include<iostream>
#include<vector>
using namesapce std;
//结构体和类使一样的,只是默认访问权限不同
struct Node {//链表的节点
int val;//数据域
Node* next;//指针域:存放下一个节点的地址
Node (int val) {
this->val = val;
next = nullptr;
}
};
class List {
private:
Node *Head = nullptr;//头节点 链表中第一个节点帮助访问整个链表
Node *Tail = nullptr;//尾节点 链表中最后一个节点帮助插入节点
public:
List (vector<int> vec);
Node *GetListHead();
void PrintList();
void DeleteNode(int val);
void AddNode(int val, int newVal);//在节点值等于val的后面增加一个新节点 例如在2后面加val值为0的节点
Node* FindNode(int val);//查找节点值为val的节点并且返回节点的地址 例如查找0
};
.cpp文件#
//.cpp实现.h中的声明
#include"List.h"
List::List(vector<int> vec) {
//用vec初始化链表
Node* node = nullptr;
for (int i = 0; i < vec.size(); ++i) {
node = new Node(vec[i]);//通过节点的构造函数创建新的节点
if (Head == nullptr) {//链表中还没有节点,新的节点就是链表的头节点并且也是链表的尾节点
Head = node;
Tail = node;
}
else {//否则链表已经有节点了,只需要向链表尾部(Tail)去添加节点并且更新Tail
Tail->next = node;
Tail = Tail->next;
}
}
}
Node* List::GetListHead() {
return Head;
}
void List::PrintList() {
Node* p = Head;
while (p != nullptr) {
cout << p->val << " ";
p = p->next;
}
}
void List::DeleteNode(int val) {
//删除节点值等于val的第一个节点
//遍历链表 找到要删除的节点
//找到要删除的节点之后,判断当前节点是不是头节点
//如果是头节点 Head = Head->next;
//如果不是头节点 p的前驱节点的next 等于p->next
//delete p;
Node* p = Head;
Node* pre = nullptr;
while (p != nullptr) {
if (p->val == val) {
if (p == Head) {
Head = Head->next;
} else {
pre->next = p->next;
}
delete p;
p = nullptr;//防止野指针
return;
}
//如果p不是要删除的节点
pre = p;//记录p的前驱节点
p = p->next;
}
}
void List::AddNode(int val, int newVal) {
Node* p = Head;
Node* newNode = new Node(newVal);
while (p != nullptr) {
if (p->val == val) {
newNode->next = p->next;
p->next = newNode;
}
p = p->next;
}
}
Node* List::FindNode(int val) {
Node* p = Head;
while (p != nullptr) {
if (p->val == val) {
return p;
}
p = p->next;
}
return nullptr;
}

结构体和类的访问权限#

成员变量和成员函数在类中的默认访问权限为private,而在结构体中默认访问权限是public

哈希表#

数组的特点:寻址容易,插入和删除困难

int a[5];
a[2] = a + 2*sizeof(int);

而链表的特点是:寻址困难,插入和删除容易

链表的地址可以是不连续的,所以寻址困难,通过next指针插入和删除容易

什么是哈希?#

概念时间复杂度

哈希是一个数据结构,对数据的增删和查找都是达到时间复杂度为O(1)

哈希里面的key值是唯一的

哈希的实现#

?哈希函数#

哈希函数的特征

输入输出

  1. int f(string) 输入域无穷大,输出域有限
  2. 有可能不同的输入,对应相同的输出
  3. 如果输入的参数是一样的,输出一定是相同的,没有任何随机的成分
  4. 均匀性:类似的输入,通过打乱,然后得到均匀的
?哈希原理#

第一步:通过哈希函数bucketnum = fun(key) % 17 得到一个值

第二步:这个值就是桶号

第三步:把这对数据放到相应的桶里

链式哈希存储

用数组存放链表的头指针,数据的下标表示用哈希函数算完之后的数值取模数组长度,来表示每个数据要放到哪个链表里

哈希扩容

链表已经很长,也就是常数M比较大了,影响效率了怎么办?

扩容一倍,比如说之前是取模17,现在可以取模34

一次扩容的代价是O(n),代价有点高,但是一次扩容是成倍增长,可以保持效率,不需要总扩容

对哈希的改进

  • 链表变成有序表(红黑树)
  • 平衡二叉树(每棵子树的左右深度差的绝对值不大于1),平衡的二叉树或者有序表的查找时间复杂度是logn
哈希冲突的解决办法(哈希碰撞)#

4种

1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)

例如:2,3,5,77,4,9,88,12,1 +3

哈希函数fun(key):key % 11

2.再哈希法

用同一个哈希函数有冲突,就换个哈希函数,直到没有冲突为止

3.链地址法(Java hashmap就是这么做的)

4.建立一个公共溢出区

#include<iostream>
#include<unordered_map>
#include<map>
using namespace std;
int main() {
unordered_map<int, int> p;
p.insert(make_pair(3, 3));
p.insert(make_pair(1, 2));
unordered_map<int, int>::iterator it = p.begin();
p[1] = 4;
p[5] = 5;//既可以插入又可以修改 慎用
if (p.find(3) != p.end()) {
cout << p.find(3)->first << p.find(3)->second << endl;
}
for (;it != p.end(); it++) {
cout << it->first << " " << it->second << endl;
}
map<int, int> _map;
_map[1] = 1;
_map[0] = 0;
_map[3] = 3;
map<int, int>::iterator it_map = map.begin();
for (;it_map != _map.end(); it_map++) {
cout << it_map->first << " " << it_map->second << endl;
}
}

二叉树#

树的定义#

树是一种数据结构,它是由n(n >= 1)个有限节点组成一个具有层次关系的集合

把它叫做“树”是因为它看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下特点:

  1. 每个节点有零个或多个子节点
  2. 没有父节点的节点称为根节点
  3. 每个非根节点有且只有一个父结点
  4. 除了根节点外,每个子节点可以分为多个不相交的子树
树的基本术语#

结点的度:

叶子:

分支结点

树的度

层次

树的高度

无序树

有序树

森林

二叉树的介绍#

二叉树的定义#

样 五种

每个结点最多有两个子树的树

有五种基本形态:

二叉树的性质#

i层节点数目 深度k节点数 n个节点高度 叶子与度为2节点数目关系

  • i层节点数目:二叉树第i层上的结点数目最多为2^(i - 1)(i≥1)。
  • 深度k节点数:深度为k的二叉树至多有2^(k)-1个结点(k≥1)。
  • n个节点高度:包含n个结点的二叉树的高度至少为log2 (n+1)。
  • 叶子与度为2节点数目关系:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则nO=n2+1。
满二叉树 完全二叉树和二叉查找树#
满二叉树#

什么样

全满的二叉树

定义:高度为h,并且由2^(h)-1个节点的二叉树,被称为满二叉树

完全二叉树#

满二叉树从最下层的最右边开始依次缺省,不能跳着缺省或到另一层缺省

定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。 特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。

二叉查找树#

左右节点与父结点关系

定义:二叉查找树(Binary Search Tree),又被称为二叉搜索树。设x为二叉查找树中的一个结点,x节点包含关键字key,节点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y]<key[x];如果y是x的右子树的一个结点,则key[y] >= key[X]。

在二叉查找树中: (01)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

(02)任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

(03)任意节点的左、右子树也分别为二叉查找树。

(04)没有键值相等的节点(no duplicate nodes)。

二叉树代码部分#

?Btree.h文件#
?Btree.cpp文件#
?main.cpp文件#

?层次遍历#

第一步:将根节点放入队列 第二步:每次从队列中弹出一个节点,记为node 第三步:看这个node有没有左孩子,如果有左孩子把左孩子放入到队列中,如果node有右孩子,把右孩子放入到队列中 第四步:重复步骤二和步骤三,直到队列为空

?Btree.h#
?Btree.cpp#

?二叉树遍历框架#

二叉树做题思路:

  • 根据题意,确定遍历模板(前中后递归)
  • 确定函数退出条件,满足结果或者root == NULL
  • 考虑当前节点需要做的事

贪心算法#

概念

贪心算法是指:在每一步求解的步骤中,它要求”贪婪""的选择最佳操作,并希望通过一系列的最优选择,能够产生一个问题的(全局的)最优解。 贪心算法每一步必须满足以下条件: 1、可行的:即它必须满足问题的约束。 2、局部最优:他是当前步骤中所有可行选择中最佳的局部选择。 3、不可取消:即选择—旦做出,在算法的后面步骤就不可改变了。

图的概念#

地图

基础概念#

微信中,许许多多的用户组成了一个多对多的朋友关系网,这个关系网就是数据结构当中的图(Graph) 。 再举一个栗子,咱们在用百度地图的时候,常常会使用导航功能。比如你在地铁站A附近,你想去的地点在地铁站F附近,那么导航会告诉你一个最佳的地铁线路换乘方案。

这许许多多地铁站所组成的交通网络,也可以认为是数据结构当中的图。图,是一种比树更为复杂的数据结构。树的节点之间是一对多的关系,并且存在父与子的层级划分;而图的顶点(注意,这里不叫节点)之间是多对多的关系,并且所有顶点都是平等的,无所谓谁是父谁是子。

图的定义#

顶点间 边 权重 有向图

所有顶点多对多,平等无父子关系

在图中,最基本的单元是顶点(vertex),相当于树中的节点。顶点之间的关联关系,被称为边(edge)。 在有些图中,每一条边并不是完全等同的。比如刚才地铁线路的例子,从A站到B站的距离是3公里,从B站到C站的距离是5公里……..这样就引入一个新概念:边的权重(Weight)。涉及到权重的图,被称为带权图(WeightedGraph)。 还有一种图,顶点之间的关联并不是完全对称的。还拿微信来举例,你的好友列表里有我,但我的好友列表里未必有你。

顶点之间的边有方向区分的是有向图

图的表示#

邻接矩阵样优缺点 有向的 无向的 邻接表 顶点与自身

拥有n个顶点的图,它所包含的连接数量最多是n (n-1)个。因此,要表达各个顶点之间的关联关系,最清晰易懂的方式是使用二维数组(矩阵)。 具体如何表示呢?我们首先来看看无向图的矩阵表示:

如图所示,顶点0和顶点1之间有边关联,那么矩阵中的元素A[0] [1] 与A[1] [0]的值就是1;顶点1和顶点2之间没有边关联,那么矩阵中的元素A[1] [2]与A[2] [1]的值就是0。 像这样表达图中顶点关联关系的矩阵,就叫做邻接矩阵。 需要注意的是,矩阵从左上到右下的一条对角线,其上的元素值必然是0。这样很容易想明白:任何一个顶点与它自身是没有连接的。 同时,无向图对应的矩阵是一个对称矩阵,V0和V1有关联,那么V1和V0也必定有关联,因此A[0] [1]和A[1] [0]的值一定相等。 那么,有向图的邻接矩阵又是什么样子呢?

从图中可以看出,有向图不再是一个对称矩阵。从V0可以到达V1,从V1却未必能到达V0,因此A[0] [1]和A[1] [0]的值不一定相等。 邻接矩阵的优点是什么呢?简单直观,可以快速查到一个顶点和另一顶点之间的关联关系。 邻接矩阵的缺点是什么呢?占用了太多的空间。试想,如果一个图有1000个顶点,其中只有10个顶点之间有关联(这种情况叫做稀疏图),却不得不建立一个1000×1000的二维数组,实在太浪费了。 邻接表 为了解决邻接矩阵占用空间的问题,人们想到了另一种图的表示方法:邻接表。

图的创建#

main.cpp#
?.h文件#
?.cpp文件#

?广度优先(对应二叉树层次遍历)#

?深度优先(对应二叉树先序遍历)#

并查集#

区分是否一个并查集

概念#

  • 并查集内存在层次
  • 区别是否一个并查集通过向上层找队长
  • 队长不同则不是一个并查集
  • 将一个队长归顺到另一个队长下并查集就二合一变更大了

相传在无忧无虑的卡通大陆上,生活着成千上百的卡通人物,有小猪佩奇,哆啦A梦,熊大熊二等等。桃花源般的生活并没有持续多久,暗夜笼罩大陆,大陆被注入了黑魔法,使得卡通人物们整天在外面游荡,碰到和自己不是一路人的,就免不了要打一架。幸运的是,他们心中的纯真还在,始终保持一个原则就是绝对不打自己的朋友,将“朋友的朋友就是我的朋友”奉为金科玉律。只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是和自己一路的人。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心地大打一架。(附上朋友关系图)

但是两个原本互不相识的人,如何判断是否属于一个朋友圈,一个群落呢?我们可以在每个朋友圈内选择出一个代表,作为这个朋友圈的队长。比如小猪佩奇所在的这个朋友圈中,我们可以选择猪爸爸为队长,每个圈子就可以这样命名”猪爸爸朋友之队”,“光头强朋友之队”…之后,如果两个人在外,遇到只要互相确定一下自己的队长是不是同一个人,就可以确定敌友关系了。 但是还有一个问题啊,大家只知道自己直接的朋友是谁,(比如猪妈妈的直接朋友是乔治,而图图一个直接朋友也没有)很多人可能根本就不认识自己圈子的队长。要判断自己的队长是谁,只能漫无目的一个个打电话问:“你是不是队长?你是不是队长?”这样一来,效率太低,甚至可能陷入无限循环中。 于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我,队长就是根节点,下面分别是二级队员、三级队员…。每个人只要记住自己的上级是谁就行了。遇到判断敌友关系的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。 因为我们关心的只是两个人之间是否连通,至于他们是如何连通的,以及每个圈子内部的结构是怎样的并不重要。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。各个朋友圈在刚开始时,都只是孤家寡人(如下图)。每一个朋友圈就一个人,所以就选自己是队长。我们假设每一个人物,都有一个自己的编号(从1~n)我们需要定义一个数组intpre[n+1],表示当前人物的上级是谁。如果pre[i]=i.,就表示编号为i的人物就是这个朋友圈的队长。如果pre[10]=1,就表示编号为10的人物的上级是1

那么,我们接下来先说说,找朋友(find)的过程。比如说在哆啦A梦朋友圈中有大雄,静香,哆啦A梦。我们假设关系是:哆啦A梦(队长)->大雄->静香。如果静香在外碰到光头强那她就要通过find函数来找队长。首先静香找到自己的上级是大雄,之后大雄找到自己的上级是哆啦A梦,这时哆啦A梦发现自己的pre[i]=i,表明自己就是队长。所以,哆啦A梦告诉大雄我是队长,大雄告诉静香哆啦A梦是队长,这就找到了队长。

我们现在在看一看,怎么将两个人”友”连接在一起?其实很简单,我们只要找到这两个人的队长,如果是不同的队长,我们就随便将一个队长归顺到另一个队长下,那么这个朋友圈就变得更大了。比如熊大在路上碰到大雄,发现2个队长不一样,哆啦A梦就拉光头强小队进了自己的朋友圈,如下图这种情况。

?.h文件#

?.cpp文件#

?最小生成树#

.h文件#

?克鲁斯卡尔算法Kruskal#

从边的角度求图的最小生成树对于任意一个连通网的最小生成树来说,在要求总的权值最小的情况下,最直接的想法就是将连通网中的所有边按照权值大小进行升序排序,从小到大依次选择。 由于最小生成树本身是一棵生成树,所以需要时刻满足以下两点:

  • 生成树中任意顶点之间有且仅有一条通路,也就是说,生成树中不能存在回路;
  • 对于具有n 个顶点的连通网,其生成树中只能有n-1条边,这n-1条边连通着n个顶点;

连接n 个顶点在不产生回路的情况下,只需要n-1条边。

克鲁斯卡尔算法的具体思路是:将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。直到具有n 个顶点的连通网筛选出来n-1条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。 判断是否会产生回路的方法为:利用并查集,判断选出来的边两个端点是否在一个集合中,如果在一个集合中就会产生回路。

?普利姆算法Prim#

?拓扑排序#

有向无环 怎么排的

有向无环图

一个项目包括A、B、C、D四个子部分来完成,并且A依赖于B和D,C依赖于D。现在要制定一个计划,写出A、B、C、D的执行顺序。这时,就可以利用到拓扑排序,它就是用来确定事物发生的顺序的。 在拓扑排序中,如果存在一条从顶点A到顶点B的路径,那么在排序结果中B出现在A的后面。

第一步:找到没有依赖于其他点的点(没有箭头指向的点)&&这个点之前没有加入到结果集中,即这个点的入度为0,然后就可以把这个点node加入结果集

第二步:遍历node.next数组,找到node指向的那些点,然后将那些点入度;

第三步:循环第一步和第二步,直到所有点都加入结果集为止

?迪杰斯特拉算法Dijkstra#

C++基础知识
https://fuwari.cbba.top/posts/c基础知识/
作者
Chen_Feng
发布于
2023-03-19
许可协议
CC BY-NC-SA 4.0