c++数据结构

文章目录

  • C++数据结构与算法
    • c++基础
      • 1. 输入输出
      • 2. 字符串
        • append/insert
        • erase
        • sscanf
        • sprintf
        • stringstream
          • 注意:
      • 3.大小写转换方法
      • 4. 字符串和数字之间的转化
      • 5.数字和字符数组(进制转化)
      • 6. 排序
      • 7. 高效查找(例题)
      • 8. 判断数字,字母
      • 9. sizeof,length,size
      • 10. 无穷大的设置
      • 11.初始化函数
      • 12. 高精度运算(从数组零位置最低位)
        • 高精度加法
        • 高精度减法
        • 高精度乘法(大数乘小数)
        • 高精度除法(大数除小数)
      • 13.随机数
        • rand
      • 14.lower_bound( )和upper_bound( )
    • STL
      • 1、STL
        • 1.1 容器
        • 1.2 算法
          • 1.21 lower_bound()
        • 1.3 迭代器
      • 2、数学模拟
      • 3、vector
      • 4、set和multiset
        • 4.1 set函数求并集、交集、差集、对称差集
          • 注意事项
        • 4.2 set(统计有多少种不同的值)
        • 4.3 multiset
      • 5. map和unordered_map
        • 5.1 map
        • 5.2 unordered_map
      • 6. 其他
        • 6.1 最大公因数和最小公倍数
        • 6.2 素数判断
        • 6.3 万能头文件
        • 6.4 auto自动类型推导
        • 6.5快速幂
    • 数据结构
      • 1、链表
        • 1.1 什么是线性结构
        • 1.2 数组和链表
        • 1.3 静态链表解题
        • 1.4 其他链表
      • 2、栈
        • 2.1 概念
        • 2.2 stack
        • 2.3 栈的应用
          • 2.3.1 括号匹配
          • 2.3.2 进制转换
          • 2.3.3 前缀式、后缀式的计算
          • 2.3.4 其他
      • 3、队列
        • 3.1 概念
        • 3.2 queue
        • 3.3 deque
        • 3.4 队列的应用及其他队列拓展
        • 3.5 优先队列
      • 1、图的存储
      • 2、图的遍历
        • 2.1 DFS深度优先搜索
          • 深搜优缺点
        • 2.2 BFS宽度优先搜索
          • 广搜优缺点
        • 2.3 DFS和BFS总结
          • 2.3.1 总结
          • 2.3.2 DFS计算全排列
          • 2.3.3 BFS计算岛屿面积(LC的绝地求生)
      • 3、最短路
        • 3.1 单源最短路径dijkstra
        • 3.2 旅游规划
        • 3.3 任意两点间最短路径floyld
      • 4、prim最小生成树(畅通工程)
      • 5、图的其他知识
      • 1、树基本概念
      • 2、二叉树存储
        • 2.1 链式
        • 2.2 一维数组
      • 3、二叉树遍历
        • 3.1 层序遍历
        • 3.2 三种递归遍历
        • 3.3 举例应用
          • 3.3.1 (7-1 列出叶结点)
          • 3.3.2 (7-2 完全二叉树的层序遍历)
      • 4、并查集
      • 5、完全二叉树——堆
      • 6、其他
        • 6.1 树状题目的DFS求解(7-5 病毒溯源)
        • 6.2 二叉树还原(7-7 树的遍历)
          • 中序和后序推前序
          • 前序和中序推后序

C++数据结构与算法

c++基础

1. 输入输出

//头文件和main函数
#include  //使用输入输出函数
using namespace std; //命名空间
int main(){
//cin
int i;
cin >> i;//不需要指定输入类型的
cout << "helloworld" << i << endl; //end line ,相当于'\n'
//cout中的<<,相当于Java的system.out.println输出中的+号
    
//cin的工作机制归结为一句话为:非空开始,空前结束(空一般指代Enter、Space、Tab键)。
//注意:cin不会删除非空字符后面的缓冲区换行符‘\n’!
    
//getline的工作机制归结为一句话为:缓冲开始,回车前结束。
//注意:getline会删除紧随该行的换行符‘\n’!。
//getline(cin,str);
    
//当用cin读入char类型时,会自动忽略空白字符,包括空格、制表符、回车等。
return 0;

2. 字符串

头文件在C++11版本后不需导入

输入使用cin,或getline(cin,字符串名),getline可输入带空格的字符串

  • str.size(),获取字符串长度
  • str[i],获取索引i的字符
  • str.substr(a,len),从str字符串中索引a截取长度为len的字符串
  • str.find(t),判断t是否在str中存在(返回的整数在[0,str.size()-1]时表示存在)返回string::npos则不存在
  • str.c_str(), str转化为char数组,char* 最好用strcpy(p,a.c_str());
  • str = char ch[] char转化为string
// 将string类型转换为字符数组
char arr[10];
string s("12345");
int len = s.copy(arr, 9);
arr[len] = '\0';
// 或者
char arr[10];
string s("12345");
strcpy(arr, s.c_str());
//strncpy(arr, s.c_str(), 10);
 
// 字符数组转化成string类型
char arr[] = "12345";
string s(arr);
// 或者
char arr[] = "12345";
string s = arr;
// 在原有基础上添加可以用
s += arr;
#include 
using namespace std;
int main()
{
    string str = "hello";
    //string 类型不能用 scanf
    cin >> str;
    //带空格输入
    getline(cin, str);//不是第一个输入时前面要加getchar();
    cout << str[2] << endl;
    //长度
    cout << str.size() << endl;
    //切片
    string t = str.substr(0, 3);
    cout << t[1] << endl;
    //寻找字串
    int i = str.find("llo");
    //没找到返回 unsigned long long
    return 0;
}

append/insert

// append函数是向string的后面追加字符或字符串。
1).向string的后面加C-string
string s = “hello “; 
const char *c = “out here “;
s.append(c); // 把c类型字符串s连接到当前字符串结尾
s = “hello out here”;

2).向string的后面加C-string的一部分
string s=”hello “;
const char *c = “out here “;
s.append(c,3); // 把c类型字符串s的前n个字符连接到当前字符串结尾
s = “hello out”;

3).向string的后面加string
string s1 = “hello “; 
string s2 = “wide “;
string s3 = “world “;
s1.append(s2); 
s1 += s3; //把字符串s连接到当前字符串的结尾
s1 = “hello wide “; 
s1 = “hello wide world “;

4).向string的后面加string的一部分
string s1 = “hello “, s2 = “wide world “;
s1.append(s2, 5, 5); 把字符串s2中从5开始的5个字符连接到当前字符串的结尾
s1 = “hello world”;
string str1 = “hello “, str2 = “wide world “;
str1.append(str2.begin()+5, str2.end()); //把s2的迭代器begin()+5和end()之间的部分连接到当前字符串的结尾
str1 = “hello world”;

5).向string后面加多个字符
string s1 = “hello “;
s1.append(4,’!’); //在当前字符串结尾添加4个字符!
s1 = “hello !!!!”;
 string ss1(s.length() - s1.length(), '0');
 s1 = ss1 + s1//加在前面
#include
using namespace std;
int main()
{
    string str="hello";
    string s="Hahah";
    str.insert(1,s);//在原串下标为1的字符e前插入字符串s
    cout<

erase

#include
#include
using namespace std;

int main(){
    string str = "hello c++! +++";
    // 从位置pos=10处开始删除,直到结尾
    // 即: " +++"
    str.erase(10);
    cout << '-' << str << '-' << endl;
    // 从位置pos=6处开始,删除4个字符
    // 即: "c++!"
    str.erase(6, 4);
    cout << '-' << str << '-' << endl;
    return 0;
}
#include
#include
using namespace std;

int main(){
    string str = "hello c++! +++";
    // 删除"+++"前的一个空格
    str.erase(str.begin()+10);
    cout << '-' << str << '-' << endl;
    // 删除"+++"
    str.erase(str.begin() + 10, str.end());
    cout << '-' << str << '-' << endl;
    return 0;
}

sscanf

  • 根据格式从字符串中提取数据。如从字符串中取出整数、浮点数和字符串等。
  • 取指定长度的字符串
  • 取到指定字符为止的字符串
  • 取仅包含指定字符集的字符串
  • 取到指定字符集为止的字符串
1.转换说明符
%a(%A)     浮点数、十六进制数字和p-(P-)记数法(C99)
%c         字符
%d         有符号十进制整数
%f         浮点数(包括float和doulbe)
%e(%E)     浮点数指数输出[e-(E-)记数法]
%g(%G)     浮点数不显无意义的零"0"
%i         有符号十进制整数(与%d相同)
%u         无符号十进制整数
%o         八进制整数
%x(%X)     十六进制整数0f(0F)   e.g.   0x1234
%p         指针
%s         字符串
%%         输出字符%
#include 
using namespace std;

int main()
{

    char str[] = "1234321";
    int a;
    sscanf(str, "%d", &a);
    
    char str[] = "123.321";
    double a;
    sscanf(str, "%lf", &a);
    
    char str[] = "AF";
    int a;
    sscanf(str, "%x", &a); //16进制转换成10进制

    char str[10];
    for (int i = 0; i < 10; i++)
        str[i] = '!';
    cout << str << endl;
    sscanf("123456", "%s", str); //---------str的值为 "123456\0!!!"
    //这个实验很简单,把源字符串"123456"拷贝到str的前6个字符,并且把str的第7个字符设为null字符,也就是\0
    cout << str << endl;

    for (int i = 0; i < 10; i++)
        str[i] = '!';
    sscanf("123456", "%3s", str); //---------str的值为 "123\0!!!!!!"
    //看到没有,正则表达式的百分号后面多了一个3,这告诉sscanf只拷贝3个字符给str,然后把第4个字符设为null字符。
    cout << str << endl;

    for (int i = 0; i < 10; i++)
        str[i] = '!';
    sscanf("aaaAAA", "%[a-z]", str); // ---------str的值为 "aaa\0!!!!!!"
    //从这个实验开始我们会使用正则表达式,括号里面的a-z就是一个正则表达式,它可以表示从a到z的任意字符,
    //在继续讨论之前,我们先来看看百分号表示什么意思,%表示选择,%后面的是条件,比如实验1的"%s",s是一个条件,表示任意字符,"%s"的意思是:只要输入的东西是一个字符,就把它拷贝给str。实验2的"%3s"又多了一个条件:只拷贝3个字符。实验3的“%[a-z]”的条件稍微严格一些,输入的东西不但是字符,还得是一个小写字母的字符,所以实验3只拷贝了小写字母"aaa"给str,别忘了加上null字符。
    cout << str << endl;

    for (int i = 0; i < 10; i++)
        str[i] = '!';
    sscanf("AAAaaaBBB", "%[^a-z]", str); // ---------str的值为 "AAA\0!!!!!!"
    //对于所有字符,只要不是小写字母,都满足"^a-z"正则表达式,符号^表示逻辑非。前3个字符都不是小写字符,所以将其拷贝给str,但最后3个字符也不是小写字母,为什么不拷贝给str呢?这是因为当碰到不满足条件的字符后,sscanf就会停止执行,不再扫描之后的字符。
    cout << str << endl;

    /*
    for (int i = 0; i < 10; i++) str[i] = '!';
    sscanf("AAAaaaBBB","%[A-Z]%[a-z]",str);// ---------段错误
    //这个实验的本意是:先把大写字母拷贝给str,然后把小写字母拷贝给str,但很不幸,程序运行的时候会发生段错误,因为当sscanf扫描到字符a时,违反了条件"%[A-Z]",sscanf就停止执行,不再扫描之后的字符,所以第二个条件也就没有任何意义,这个实验说明:不能使用%号两次或两次以上
    cout<

sprintf

  • 将数字变量转换为字符串。
  • 得到整型变量的16进制和8进制字符串。
  • 连接多个字符串。
#include 

uisng namespace std;

int main()
{
    char str[256] = {0};
    int data = 1024;

    //将data转换为字符串
    sprintf(str, "%d", data);

    //获取data的十六进制
    sprintf(str, "0x%X", data);

    sprintf(str, "%x", data); //10进制转换成16进制,如果输出大写的字母是sprintf(str,"%X",a)

    //获取data的八进制
    sprintf(str, "0%o", data);

    const char *s1 = "Hello";
    const char *s2 = "World";
    //连接字符串s1和s2
    sprintf(str, "%s %s", s1, s2);
    cout << str << endl;
    return 0;
}

stringstream

stringstream类的对象我们还常用它进行string与各种内置类型数据之间的转换。

#include 
using namespace std;
//数字转字符串
int main()
{
    double a = 123.32;
    string res;
    stringstream ss;
    ss << a;//或者 res = ss.str();
    ss >> res;
    return 0;
}
#include 
using namespace std;
//字符串转数字
int main()
{
    string a = "123.32";
    double res;
    stringstream ss;
    ss << a;
    ss >> res;
    return 0;
}
注意:

重复利用stringstream对象

如果我们打算在多次转换中使用同一个stringstream对象,记住再每次转换前要使用clear()方法;
在多次转换中重复使用同一个stringstream(而不是每次都创建一个新的对象)对象最大的好处在于效率。stringstream对象的构造和析构函数通常是非常耗费CPU时间的。

在类型转换中使用模板

我们可以轻松地定义函数模板来将一个任意的类型转换到特定的目标类型。例如,需要将各种数字值,如int、long、double等等转换成字符串,要使用以一个string类型和一个任意值t为参数的to_string()函数。to_string()函数将t转换为字符串并写入result中。使用str()成员函数来获取流内部缓冲的一份拷贝:

template 

void to_string(string &result, const T &t)

{
    ostringstream oss; //创建一个流

    oss << t; //把值传递如流中

    result = oss.str(); //获取转换后的字符转并将其写入result
}
//这样,我们就可以轻松地将多种数值转换成字符串了:
to_string(s1, 10.5); //double到string
to_string(s2, 123);  //int到string
to_string(s3, true); //bool到string

可以更进一步定义一个通用的转换模板,用于任意类型之间的转换。函数模板convert()含有两个模板参数out_type和in_value,功能是将in_value值转换成out_type类型:

template 
  out_type convert(const in_value &t)
{
          stringstream stream;
          stream << t;      //向流中传值
          out_type result;  //这里存储转换结果
          stream >> result; //向result中写入值
          return result;
     
}

3.大小写转换方法

  • 如果使用string类,可以使用#include 里的如下方法进行大小写转换;transform(str.begin(),str.end(),str.begin(),::tolower);

    记得::tolower前面有::, 而且是::tolower,不是::tolower()

    #include 
    #include 
    
    using namespace std;
    string s;
    int main()
    {
        cout << "请输入一个含大写的字符串:";
        string str;
        cin >> str;
        ///转小写
        transform(str.begin(), str.end(), str.begin(), ::tolower);
        cout << "转化为小写后为:" << str << endl;
        transform(str.begin(), str.end(), str.begin(), ::toupper);
        cout << "转化为大写后为:" << str << endl;
        return 0;
    }
    
  • string类也可以自己手写两个转化为大写和小写transform()方法,如下所示:

    #include 
    #include 
    #include 
    using namespace std;
    void mytolower(string &s)
    {
        int len = s.size();
        for (int i = 0; i < len; i++)
        {
            if (s[i] >= 'A' && s[i] <= 'Z')
            {
                s[i] += 32; //+32转换为小写
                // s[i]=s[i]-'A'+'a';
            }
        }
    }
    void mytoupper(string &s)
    {
        int len = s.size();
        for (int i = 0; i < len; i++)
        {
            if (s[i] >= 'a' && s[i] <= 'z')
            {
                s[i] -= 32; //+32转换为小写
                // s[i]=s[i]-'a'+'A';
            }
        }
    }
    
    int main()
    {
        cout << "请输入一个含大写的字符串:";
        string str;
        cin >> str;
        ///转小写
        mytolower(str);
        cout << "转化为小写后为:" << str << endl;
        mytoupper(str);
        cout << "转化为大写后为:" << str << endl;
        return 0;
    }
    
  • 如果用char数组,也可以自己手写两个转化为大写和小写方法,此种方法用到了tolower(char c)和toupper(char c)两个方法:

    #include 
    #include 
    #include 
    using namespace std;
    void mytolower(char *s)
    {
        int len = strlen(s);
        for (int i = 0; i < len; i++)
        {
            if (s[i] >= 'A' && s[i] <= 'Z')
            {
                s[i] = tolower(s[i]);
                // s[i]+=32;//+32转换为小写
                // s[i]=s[i]-'A'+'a';
            }
        }
    }
    void mytoupper(char *s)
    {
        int len = strlen(s);
        for (int i = 0; i < len; i++)
        {
            if (s[i] >= 'a' && s[i] <= 'z')
            {
                s[i] = toupper(s[i]);
                // s[i]-=32;//+32转换为小写
                // s[i]=s[i]-'a'+'A';
            }
        }
    }
    
    int main()
    {
        cout << "请输入一个含大写的字符串:";
        char s[201];
        gets(s);
        ///转小写
    
        mytolower(s);
        cout << "转化为小写后为:" << s << endl;
        mytoupper(s);
        cout << "转化为大写后为:" << s << endl;
        return 0;
    }
    
  • 如果用char数组,也可以使用s[i]+=32或者s[i]=s[i]-‘A’+'a’的形式,实现两个转化为大写和小写方法,如下所示:

    #include 
    #include 
    #include 
    using namespace std;
    void mytolower(char *s)
    {
        int len = strlen(s);
        for (int i = 0; i < len; i++)
        {
            if (s[i] >= 'A' && s[i] <= 'Z')
            {
                s[i] += 32; //+32转换为小写
                // s[i]=s[i]-'A'+'a';
            }
        }
    }
    void mytoupper(char *s)
    {
        int len = strlen(s);
        for (int i = 0; i < len; i++)
        {
            if (s[i] >= 'a' && s[i] <= 'z')
            {
                s[i] -= 32; //+32转换为小写
                // s[i]=s[i]-'a'+'A';
            }
        }
    }
    
    int main()
    {
        cout << "请输入一个含大写的字符串:";
        char s[201];
        gets(s);
        ///转小写
        mytolower(s);
        cout << "转化为小写后为:" << s << endl;
        mytoupper(s);
        cout << "转化为大写后为:" << s << endl;
        return 0;
    }
    

4. 字符串和数字之间的转化

字符转为对应的数字,直接char-'0’即可

  • 字符串转为数据
  • stoi,转为int,
  • stol,转为long
  • stof,转为float
  • stod,转为double
  • 数据转为字符串 to_string,整数和小数均可
#include 
using namespace std;
int main()
{ //字符串转化为数字
    //整数和小数
    string str = "123.56";
    double i = stod(str); //string to double
    cout << i << endl;
    string t = to_string(i); //数字转化为字符串

    //数字字符转换为对应的数字
    int a = str[0] - '0';
    return 0;
}

5.数字和字符数组(进制转化)

  • itoa 将整形转换为字符串型 string itoa(int x,char *string,int jz);
int temp;
char tempC[100];
itoa(temp,tempC,10);
  • atoi 将字符数组转换成一个整数值 int atoi(const char* str)

    如果第一个非空格字符存在,是数字或者正负号则开始做类型转换,之后检测到非数字(包括结束符 \0) 字符时停止转换,返回整型数,否则,返回零。

char cc[20]="-100";
int dd;
dd=atoi(cc);
cout<

6. 排序

数组排序

#include 
#include 

using namespace std;
int main()
{
    int arr[5] = {5, 3, 1, 2, 4};
    //sort(第一个数地址,最后一个数地址,排序方法);默认升序
    //降序排列
    sort(arr, arr + 5, greater{});

    for (int i = 0; i < 5; i++)
    {
        cout << arr[i] << "";
    }
    cout << endl;
    return 0;
}

结构体排序

#include 
#include 

using namespace std;
struct Node
{
    int id;
    int age;
};
bool cmp(const Node &a, const Node &b)
{
    if (a.id != b.id)
        return a.id < b.id; //按照学号升序排序
    else
        return a.age < b.age; //学号相等,按照年龄升序
}
int main()
{
    Node arr[5];
    arr[0] = {1, 3};
    arr[1] = {2, 5};
    arr[2] = {0, 1};
    arr[3] = {-6, 2};
    arr[4] = {2, 4};

    //sort(第一个数地址,最后一个数地址,排序方法);
    sort(arr, arr + 5, cmp);

    for (int i = 0; i < 5; i++)
    {
        cout << arr[i].id << " " << arr[i].age << endl;
    }
    cout << endl;
    return 0;
}

7. 高效查找(例题)

#include 
using namespace std;
int main()
{
    //给定长度为n的数组arr,和长度为k的数组t
    //查找tt元素在arr中出现的次数

    //哈希查找
    int n;
    cin >> n;
    int arr[n + 1];
    int book[101] = {0};
    for (int i; i <= n; i++)
    {
        cin >> arr[i - 1];
        book[arr[i - 1]]++;
    }
    int k;
    cin >> k;
    int t[k + 1];
    for (int i; i <= k; i++)
    {
        cin >> t[i - 1];
    }
    for (int i = 1; i <= k; i++)
    {
        cout << book[t[i - 1]] << (i == k ? '\n' : ' ');
    }
}

8. 判断数字,字母

#include
isalnum(s[j])//数字字母
isalpha(s[j])//字母
isdigit(s[j])//数字
               

9. sizeof,length,size

  • sizeof计算变量、常量名或是数据类型名占用的空间字节数;
  • strlen传入C语言字符串(字符数组名或字符指针,以\0结尾),返回从传入的第一个字符到\0的长度;
  • sizelength都是string类的成员函数,两函数完全相同,返回C++字符串的长度。起初string是没有size函数的,后来C++引入STL后为保持各容器统一才加入的

10. 无穷大的设置

0x3f3f3f3f主要有如下好处:

  • 0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。
  • 0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。
  • 可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。

11.初始化函数

memset函数是把array的每个字节都赋值成第二个参数(因为他的头文件是cstring嘛),一个int数字,每个字节都是0x3f,这个数字的值就是0x3f3f3f3f。

memcpy用来做内存拷贝,你可以拿它拷贝任何数据类型的对象,可以指定拷贝的数据长度;例:char a[100],b[50]; memcpy(b, a, sizeof(b));注意如用sizeof(a),会造成b的内存溢出。

memcmp是比较内存区域buf1和buf2的前count个字节。该函数是按字节比较的

12. 高精度运算(从数组零位置最低位)

高精度加法

C从0到C .size( )为低位到高位

#include

using namespace std;

vector add(vector &A, vector &B)
{
    if (A.size() < B.size())
        return add(B, A);
    int t = 0;
    vector C;
    for (int i = 0; i < A.size(); i++)
    {
        t += A[i];
        if (i < B.size())
            t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }
    if (t)
        C.push_back(t);
    return C;
}

int main()
{
    vector A, B, C;
    string str1, str2;
    cin >> str1 >> str2;
    for (int i = str1.length() - 1; i >= 0; i--)
        A.push_back(str1[i] - '0');
    for (int i = str2.length() - 1; i >= 0; i--)
        B.push_back(str2[i] - '0');
    C = add(A, B);
    return 0;
}

高精度减法

比较A,B位数大小函数
(高精度减法需保证A位数大于B,若A

bool cmp( vector &A, vector &B ) {
    if ( A.size() != B.size()) return A.size() > B.size();
    // 未返回则说明A,B位数相同
    // 从高位开始比较,直到遇到一个不同的数再比较大小
    for ( int i = A.size()-1; i>=0; i-- ) {
        if ( A[i] != B[i] ) return A[i] > B[i]; 
    }
    return true;
}

vector sub( vector &A, vector &B ) {
    vector C;
    //  t表示借位,为0或1
    int t = 0;
    for ( int i=0; i < A.size(); i++ ) {
        t  = A[i] - t;
        if ( i < B.size() ) t -= B[i];
        C.push_back( (t+10) % 10 );
        if( t < 0 ) t = 1;
        else t = 0;
    }
    // 去掉前导0
    while ( C.size() > 1 && C.back() == 0 ) C.pop_back();
    return C;
}

高精度乘法(大数乘小数)

一个大数×一个小数
对于A的每一位数都跟整个b相乘而不是一位一位计算

vector mul( vector &A, int b ) {
    vector C;
    int t = 0;
    for ( int i =0; i < A.size() || t; i++ ) {
        if( i < A.size()) t += A[i] * b;
        C.push_back( t % 10);
        t /= 10;
    }
    while ( C.size() > 1 && C.back() == 0 ) C.pop_back();
    return C;
}

高精度除法(大数除小数)

// r 存储余数
vector div( vector &A, int b, int &r ) {
    vector C;
    r = 0;
    for ( int i = A.size()-1; i >= 0; i-- ) {
        r = r * 10 + A[i];
        C.push_back( r / b );
        r %= b;
    }
    // C存储由高位到低位
    reverse(C.begin(), C.end());
    while ( C.size() > 1 && C.back() == 0 ) C.pop_back();
    return C;
}

13.随机数

rand

  • rand()不需要参数,它会返回一个从0到最大随机数的任意整数,最大随机数的大小通常是固定的一个大整数。

  • 如果你要产生0~99这100个整数中的一个随机整数,可以表达为:int num = rand() % 100; 这样,num的值就是一个0~99中的一个随机数了。

  • 一般性:rand() % (b-a+1)+ a ; 就表示 a~b 之间的一个随机整数。

  • #include "stdafx.h"
    #include "iostream"
    #include "ctime"
    #include "cstdlib"
    using namespace std;
    #define N  999 //精度为小数点后面3位
    int main()
    {
    	float num;
    	int i;
    	float random[10];
    	srand(time(NULL));//设置随机数种子,使每次产生的随机序列不同
    	for (int i = 0; i < 10; i++)
    	{
    		random[i] = rand() % (N + 1) / (float)(N + 1);
    	}
    	for (int i = 0; i < 10; i++)
    	{
    		cout << random[i] << endl; //输出产生的10个随机数
    	}
        return 0;
    }
    

14.lower_bound( )和upper_bound( )

lower_bound( )和upper_bound( )都是利用二分查找的方法在一个排好序的数组中进行查找的。

在从小到大的排序数组中,

lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

在从大到小的排序数组中,重载lower_bound()和upper_bound()

lower_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

upper_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

#include

using namespace std;
const int maxn = 100000 + 10;
const int INF = 2 * int(1e9) + 10;
#define LL long long

int cmd(int a, int b)
{
    return a > b;
}

int main()
{
    int num[6] = {1, 2, 4, 7, 15, 34};
    sort(num, num + 6);                           //按从小到大排序 
    int pos1 = lower_bound(num, num + 6, 7) - num;    //返回数组中第一个大于或等于被查数的值 
    int pos2 = upper_bound(num, num + 6, 7) - num;    //返回数组中第一个大于被查数的值
    cout << pos1 << " " << num[pos1] << endl;
    cout << pos2 << " " << num[pos2] << endl;
    sort(num, num + 6, cmd);                      //按从大到小排序
    int pos3 = lower_bound(num, num + 6, 7, greater()) - num;  //返回数组中第一个小于或等于被查数的值 
    int pos4 = upper_bound(num, num + 6, 7, greater()) - num;  //返回数组中第一个小于被查数的值 
    cout << pos3 << " " << num[pos3] << endl;
    cout << pos4 << " " << num[pos4] << endl;
    return 0;
} 

STL

1、STL

Standard Template Library,标准模板库,是C++开发人员编码好的一些封装数据结构的类和算法函数的库。

主要有以下三部分:(其实不止,如果今后想深入学习C++,了解以下这些是远远不够的,但基本足够做题了)

1.1 容器

存放数据的一段内存地址,STL中每种容器都是对一种数据结构的实现

  1. vector:可变长数组
  2. string:字符串
  3. stack:栈
  4. list:链表(不常用)双向的
  5. queue:队列
  6. map和unordered_map:可以理解为哈希表
  7. deque:双端队列(不常用)
  8. priority_queue:优先队列(堆)(不常用)
  9. 。。。等等

数据结构用来解题,直接从STL里面取出使用即可

1.2 算法

算法即解决问题的方法,基本都在头文件中

  1. sort:排序
  2. reverse:逆转
  3. **find:**寻找
  4. **max:**最大值
  5. **min:**最小值
  6. **copy:**复制
1.21 lower_bound()

lower_bound()返回值是一个迭代器,返回指向大于等于key的第一个值的位置

对应lower_bound()函数是upper_bound()函数,它返回大于等于key的最后一个元素

#include 
#include 
using namespace std;
int main()
{
	int a[]={1,2,3,4,5,7,8,9};
	printf("%d",lower_bound(a,a+8,6)-a); 
	
 return 0;	
} 
#include
#include
#include
using namespace std;
 
int main()
{
    vector A;
    A.push_back(1); 
    A.push_back(2); 
    A.push_back(3); 
    A.push_back(4); 
    A.push_back(5); 
    A.push_back(7); 
	A.push_back(8); 
	A.push_back(9); 
    
    int pos = lower_bound(A.begin() , A.end() , 6)-A.begin();
    cout << pos << endl;
    
    
    
    return 0;  
 }

1.3 迭代器

访问容器内元素的统一方法,将STL内的容器和算法间连接起来。可以看作指针(仅仅用来做题的话,其实是错误的)

每种容器都有自己的迭代器

容器名<泛型>::iterator //可使用auto代替
.begin() //返回容器中第一个元素的迭代器(指向它的指针)
.end() //返回容器中最后一个元素,再向下一个位置的迭代器(指向它的指针)

2、数学模拟

数学模拟题,即编程实现给定题目的情景,需要一些数学知识,如:素数的判断、最大公约数等

天梯赛中L1和L2大多为模拟题,即题目给定一个情景,分析出使用什么数据结构来实现计算,题目数据量并不大,大部分题并不会要求采用高效率的算法。其中数学模拟题涉及的数学知识非常简单,但ACM、CCPC这样的算法竞赛,涉及数学知识就很复杂了,本周的3道提高题主要帮助大家了解算法竞赛的入门知识点,为想参赛的同学一些帮助。

3、vector

需导入头文件

可变长数组,容量依据插入的数据量而变化

可在定义时指定初始容量

vector ve; //存储int的vetor
vector ve(10); //存储double的vetor,初始就有10个数,为0.0
vector> ve; //存储vector的vector,没错,泛型可以是任何已定义的数据类型,包括指针
vector> A(a, vector(b));//初始化
v.resize(n, vector(m));
    vector > res;
        res.resize(r);//r行
        for (int k = 0; k < r; ++k){
            res[k].resize(c);//每行为c列
        }

vector是一个类(模板类),常用类函数如下:

  • ve[i],访问i索引位置的元素,索引从0开始(其实有个函数,ve.at(i),功能相同)
  • push_back(data),放入数据
  • pop_back(),删除最后一个数据
  • size(),获取容量大小
  • insert(it,data),在it迭代器位置插入数据data,push_back(data)相当于insert(end(),data)
  • resize(sum),更改容量为sum并置为0,如果本身数据量大于sum,会丢失数据
  • ==和!=可以用来判断两个vector是否相同,>和<也可以用于vector

4、set和multiset

4.1 set函数求并集、交集、差集、对称差集

beg1,end1容器1的开始、结束迭代器,beg2,end2容器2的开始、结束迭代器

  • 求交集set_intersection()

    函数原型:set_intersection(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest)

    作用:将容器1和容器2的交集存到目标容器,dest为目标容器的起始迭代器。

  • 求并集set_union()

    函数原型:set_union(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest)

    作用:将容器1和容器2的并集存到目标容器,dest为目标容器的起始迭代器。

  • 求差集set_difference()

    函数原型:set_difference(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest)

    作用:将容器1和容器2的差集存到目标容器,dest为目标容器的起始迭代器。

  • 求对称差集set_symmetric_difference()

    函数原型:set_symmetric_difference(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest)

    作用:将容器1和容器2的对称差集存到目标容器,dest为目标容器的起始迭代器。

注意事项

上述四种函数中目标容器必须提前准备好大小,考虑极限情况,函数返回目标容器最后一个被写入的位置(迭代器);

两个源容器必须有序。

4.2 set(统计有多少种不同的值)

需导入头文件

同样,容量依据插入的数据量而变化

  1. 会进行自动排序
  2. 不会出现重复的值
set s; //存储int的set

访问元素时,只能使用迭代器访问

for(auto it=s.begin();it!=s.end();it++){
    cout << *it << endl;
}

其常用类函数如下:

  • insert(data),插入数据data
  • size(),获取当前容量
  • find(data),查找data,返回迭代器,不存在则返回s.end()
  • erase(it),删除迭代器it位置的元素

set的底层是红黑树,查找效率很高

总体来说,set使用的很少,基本没有(有估计就是比较难的题,因为红黑树难)

在使用set容器时,如果需要用到结构体时。需要重载 "<"

//判断矩阵是否相同
set s;   
struct matrix
{
    int a[5][5];
    bool operator<(matrix x) const
    {
        for (int i = 1; i <= 3; i++)
            for (int j = 1; j <= 3; j++)
                if (a[i][j] != x.a[i][j])
                    return a[i][j] < x.a[i][j];
        return false;
    }
};

4.3 multiset

导入头文件即可使用

用法和set一摸一样,只是可以出现重复值

5. map和unordered_map

5.1 map

需导入头文件

每个数据由键(索引)和值组成,其实是pair对组

  1. 索引位置会自动排序
  2. 索引不会出现重复值

可以看成,自定义索引数据类型的数组(可变长数组)

map book; //索引类型为string,值类型为int
book[str]++;

访问元素时,只能使用迭代器访问

for(auto it=s.begin();it!=s.end();it++){
    //first访问索引位置,second访问该索引下的值
    cout << it->first << " " << it->second << endl;
}

常用类函数如下:

  • mp.count()返回0和1;
  • mp.find()返回一个迭代器,若容器中不存在该元素则返回mp.end();
  • book[index]=data,index索引位置的值置为data
  • size(),返回当前容量(只要访问一个索引,容量就会加一;访问但没有赋值,系统会置为0)

map的底层也是红黑树,只是每个结点是一个对组

使用的频率比set高,基本每年天梯赛必用

5.2 unordered_map

需导入头文件

用法和map一摸一样,只是索引位置不会自动排序

但是,迭代器遍历时,输出顺序并不是使用索引位置插入数据的顺序

实现的是哈希表,主要用来当作标记数组

(当标记的值是字符串、结构体等非单个数据时;或标记的值过大而普通数组容量无法容纳时)

6. 其他

6.1 最大公因数和最小公倍数

最大公因数可使用辗转相除法计算,C++的****头文件中,__gcd函数实现了这个算法

long long gcd(long long t1, long long t2) //求最大公约数
{
    return t2 == 0 ? t1 : gcd(t2, t1 % t2);
}

最小公倍数可根据最大公因数计算,为a*b/__gcd(a,b)

__gcd(a,b); //计算a和b的最大公因数,前两两个下划线
a/__gcd(a,b)*b; //计算a和b的最小公倍数,为了防止a*b越界(例如a*b超过了int范围),一般这样写

6.2 素数判断

天梯赛中涉及素数判断的,只需要最朴素的判断即可满足所需的时间效率

bool prime(int n) { //判断n是否为素数
    if(n == 1) {
        return false;
    }
    for(int i = 2; i * i <= n; i++) { //主要通过i*i<=n,提高效率
        if(n % i == 0) {
            return false;
        }
    }
    return true;
}

其余高效的算法是素数筛(有很多方法),大家可看视频或自己百度了解

  • 朴素筛

基本思想:i的倍数全部筛选掉

void init(int n)//朴素筛
{
    //1表示被筛选掉了,0表示质数
    st[1] = 1;
    for (int i = 2; i <= n ; i++)
        for (int j = 2; j <= n / i; j++)
            st[i * j] = 1;
}
  • 埃氏筛(用质数筛合数)

基本思想:从2开始,将每个质数的倍数都标记成合数,以达到筛选素数的目的。

void init(int n) 
{
    //1表示被筛选掉了,0表示质数
    st[1] = 1;
    for (int i = 2; i <= n/i ; i++)
        if (!st[i])//素数再继续筛
        {
            for (int j = i; j <= n / i; j++)
                st[i * j] = 1;
        }
}
  • 欧拉筛/线性筛(最小的质因子筛合数)

基本思想:在埃氏筛法的基础上,让每个合数只被它的最小质因子筛选一次,以达到不重复的目的。

vector prime;
void init(int n) 
{
    //1表示被筛选掉了,0表示质数
    st[1] = 1;
    for (int i = 2; i <= n ; i++)
    {
        if (!st[i]) //素数再继续筛
            prime.push_back(i);
        for (int j = 0; prime[j] <= N / i; j++)
        {
            st[i * prime[j]] = 1;
            if (i % prime[j] == 0)
                break;
        }
    }
}

6.3 万能头文件

#include //一个顶100个

该头文件内部包括了很多常用的头文件,基本覆盖做题需要的STL的全部

6.4 auto自动类型推导

C++11新特性,会根据变量被赋予的值自动推导出该变量的数据类型

用于简化遍历容器时,迭代器的书写。不建议在其他地方使用

for(vector>>::iterator it=s.begin();it!=s.end();it++){
    cout << *it << endl;
}
//vector>>的迭代器书写很复杂,使用auto直接代替
for(auto it=s.begin();it!=s.end();it++){
    cout << *it << endl;
}

6.5快速幂

#include 
using namespace std;
using ll = long long;
const int mod = 1e3;
// 分治递归
ll f(int a, int n) //算a^n logn
{
    // 算 a^n
    if (n == 0)
        return 1;
    if (n == 1)
        return a % mod;
    ll x = f(a, n / 2); //算a^(n/2)
    if ((n & 1) == 0)   //n&1等价于n%2
        return x * x % mod;
    else
        return x * x % mod * a % mod;
}
ll qmi(int a, int n) //二进制
{
    ll res = 1;
    while (n)
    {
        if (n & 1)
            (res *= a) %= mod;
        (a *= a) %= mod;
        n >>= 1;
    }
    return res;
}
int main()
{
    int a, n;
    cin >> a >> n;
    cout << f(a, n);
    return 0;
}

数据结构

1、链表

1.1 什么是线性结构

相同性质数据元素的集合,元素与元素(结点与结点)之间一对一连续存储(逻辑上的连续)

常用的线性结构有:链表、栈、队列、双端队列、数组

1.2 数组和链表

数组中的元素在存储地址上是连续的

  • 例如数组arr[10],容量为10的数组。arr[3]相当于*(arr+3)
  • 数组访问快,插入和删除慢

链表中的元素在存储地址上不是连续的,因此每个结点必须存储下一个节点的地址(指针)

  • 链表访问慢,插入和删除快
struct LinkNode {
    T data;//存储数据,数据类型可以是任何
    struct LinkNode* next;//下一个结点的地址,最后一个结点的下一个是NULL(0)
};

while(head) { //遍历链表,head为第一个结点
    //...
    head = head->next;
}

void insertNode(LinkNode* cur, LinkNode* newNode) { //将newNode结点插入到cur后
    newNode->next = cur->next;
    cur->next = newNode;
}

1.3 静态链表解题

PTA平台的链表题目,大多使用静态链表解题,即使用数组索引模拟地址

  1. data[index],存储index索引位置的数据
  2. next_add[index],存储index索引位置结点的下一个结点的索引

链表排序例题链接

如下为该题题解:

#include 
#include 
#include 

using namespace std;

struct Node {
    int add;//该节点地址
    int data;//数据
};

bool cmp(const Node &a, const Node &b) {
    return a.data < b.data;
}

int main() {
    //静态链表
    int n, f_add; //第一个结点的地址
    scanf("%d %d", &n, &f_add);
    int data[100001], next_add[100001];
    for(int i = 1; i <= n; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);//输入当前结点地址、数据、下一个节点地址
        data[a] = b;
        next_add[a] = c;
    }
    vector ret;
    while(f_add != -1) {
        Node t;
        t.add = f_add;
        t.data = data[f_add];
        ret.push_back(t);
        f_add = next_add[f_add];
    }
    sort(ret.begin(), ret.end(), cmp);
    if(ret.size() == 0) {
        printf("0 -1\n");
    } else {
        printf("%d %05d\n", (int)ret.size(), ret[0].add);
        for(int i = 0; i < (int)ret.size() - 1; i++) {
            printf("%05d %d %05d\n", ret[i].add, ret[i].data, ret[i + 1].add);
        }
        printf("%05d %d -1\n", ret.back().add, ret.back().data);
    }
    return 0;
}

1.4 其他链表

以上涉及的是单链表和静态链表,除此还有双端链表、循环链表等多种数据结构(以后的数据结构课都会学,找工作必备)

双端链表:每个结点既存储下一个结点的地址,也存储上一个结点的地址

循环链表:最后一个结点的下一个不是NULL,而是头结点,整个链表是一个圈(可了解下约瑟夫环的循环链表解法)

2、栈

2.1 概念

受限制的线性结构,后进先出,数据的插入和删除只能在一端进行(栈顶)

也叫堆栈,可采用数组实现、也可采用链表实现,限制住不能随意访问栈内数据,后进先出即可

  • 数据入栈、压栈:数据放入栈里面
  • 数据弹栈、出栈:栈顶的数据被删除(最后一个放入的数据)

2.2 stack

STL里面的stack实现了栈,需导入头文件

stack st;//和其他容器一样,写泛型

while(st.size() > 0) { //遍历,条件也可为 !st.empty()
    //...
    st.pop();
}

相关函数如下:

  • push(data):data入栈
  • top():获取栈顶元素
  • pop():弹出栈顶元素(若本身栈是空的,会抛出异常)
  • size():获取栈内元素数
  • empty():栈是否为空,空则返回true(也可作为遍历的条件)

2.3 栈的应用

2.3.1 括号匹配

遍历给定的括号序列,规则如下:

  • 遇到左括号,入栈
  • 遇到右括号,与栈顶的左括号匹配是不是一对

若需要匹配时不能匹配为一对或栈为空或遍历完后栈不为空,则括号序列不合法

bool func(const string &str) { //判断str括号序列是否合法
    stack st;
    unordered_map book; //括号配对表
    book['('] = ')';
    book['{'] = '}';
    book['['] = ']';
    for(int i = 0; i < (int)str.size(); i++) { //遍历序列
        char ch = str[i];
        if(ch == '{' || ch == '(' || ch == '[') { //左括号直接入栈
            st.push(ch);
        } else { //右括号则与栈顶的左括号进行匹配
            if(st.size() == 0 || book[st.top()] != ch) { //栈为空或不能配均不合法
                return false;
            }
            st.pop();
        }
    }
    if(st.size() > 0) { //栈不为空,说明左括号多余,也不合法
        return false;
    }
    return true;
}
2.3.2 进制转换

依据转换的规律,将余数入栈后输出

char book[16] = {
    '0', '1', '2', '3', '4',
    '5', '6', '7', '8', '9',
    'A', 'B', 'C', 'D', 'E', 'F'
}; //字符对照数组

string func(int n, int t) { //十进制的n转换为t进制
    stack st;
    while(n > 0) {
        st.push(book[n % t]); //余数入栈
        n = n / t;
    }
    string ret;
    while(st.size() > 0) {
        ret += st.top();
        st.pop();
    }
    return ret;
}
2.3.3 前缀式、后缀式的计算

前缀式从后向前遍历,计算规则如下:

  1. 数字直接入栈
  2. 运算符则弹出栈内两个元素,将进行了该运算操作后的结果入栈(注意除法和减法,第一个弹出的数是被除数和被减数)

后缀式从前向后遍历,计算规则同前缀式相同(注意除法和减法,第一个弹出的数是除数和减数,与前缀式相反)

如果遇到运算符运算时,出现除数为0或栈内元素数小于2,则运算式错误;或是遍历结束后,栈内元素数不是1,也错误

栈也可帮助中缀式转为后缀式(逆波兰算法),比较复杂,遇事不决找百度(谷歌也行,不过没必要)

2.3.4 其他

栈的应用有很多,如重排车厢、开关盒布线等,有简单的也有复杂的,大家可自行百度了解

3、队列

3.1 概念

受限制的线性结构,先进先出,数据的插入和删除只能相反的两端进行(队尾插入、队首删除)

可采用数组实现、也可采用链表实现,限制住不能随意访问队列内数据,先进先出即可

  • 数据入队:队尾插入新数据
  • 数据出队:队首第一个数据被删除

3.2 queue

STL的queue实现了队列数据结构,需导入头文件

queue qu;//和其他容器一样,写泛型

while(qu.size() > 0) { //遍历,条件也可为 !qu.empty()
    //...
    qu.pop();
}

常用函数如下:

  • push(data):data入队
  • front():获取队首元素
  • pop():队首元素出队
  • size():获取队列内元素数
  • empty():队列是否为空,空则返回true(也可作为遍历的条件)

3.3 deque

  • push_back() 尾部插入
  • push_front() 头部插入
  • back() 尾部元素
  • front() 头部元素
  • pop_back() 尾部删除
  • pop_front() 头部删除

3.4 队列的应用及其他队列拓展

队列的单独应用并不多,不过以后学习的的BFS、分支限界法会应用队列

队列和链表一样,有很多衍生品,如:双端队列、循环队列等

双端队列:队列的两端均可进行数据的插入和删除,STL的deque实现了这一数据结构

循环队列:类似循环链表,队尾结点与队首结点相连

3.5 优先队列

既然是队列那么先要包含头文件#include , 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队

优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的

和队列基本操作相同:

  • top 访问队头元素

  • empty 队列是否为空

  • size 返回队列内元素个数

  • push 插入元素到队尾 (并排序)

  • emplace 原地构造一个元素并插入队列

  • pop 弹出队头元素

  • swap 交换内容

    定义:priority_queue
    Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆一般是:

//小根堆
priority_queue ,greater > q;
//大根堆
priority_queue ,less >q;

//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)

bool operator< (node a,node b)
{
    if(a.x == b.x)  return a.y >= b.y;
    else return a.x > b.x;
}
struct node
{
      int x, y;
      node(int x, int y):x(x),y(y){}
      bool operator< (const node &b) const   //写在里面只用一个b,但是要用const和&修饰,并且外面还要const修饰;
      {
           if(x == b.x)  return y >= b.y;
           else return x > b.x;
      }
};

1、图的存储

边集数组:存储边,大多是题目给定的存储方式

struct Line {
    int p1;//起点
    int p2;//终点
    int weight;//权重
};

邻接矩阵:n行n列(n是图中点的个数)的矩阵,二维数组存储。1或权重表示可达,0或∞表示不可达

邻接表:存储每个点可到达的点,链表实现,但做题中一般使用数组

vector ve[n+1];//ve[n]存储了点n可到达的点

2、图的遍历

2.1 DFS深度优先搜索

从图的某一点开始,一条路走到黑,然后回头走之前没走过的支路(该支路通向没有走过的点),直到所有的点都走过(遍历过)

深搜优缺点

优点

  • 能找出所有解决方案
  • 优先搜索一棵子树,然后是另一棵,所以和广搜对比,有着内存需要相对较少的优点

缺点

  • 要多次遍历,搜索所有可能路径,标识做了之后还要取消。
  • 在深度很大的情况下效率不高

使用场景:性格测试的游戏

具体看代码:

#include 
#include 

using namespace std;

int n = 9;
vector ve[101]; //邻接数组
int mar[101][101] = {
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
    {0, 1, 0, 1, 0, 0, 0, 1, 0, 1},
    {0, 0, 1, 0, 1, 0, 0, 0, 0, 1},
    {0, 0, 0, 1, 0, 0, 0, 1, 1, 1},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
    {0, 1, 0, 0, 0, 0, 0, 1, 0, 0},
    {0, 0, 1, 0, 1, 0, 1, 0, 1, 0},
    {0, 0, 0, 0, 1, 1, 0, 1, 0, 0},
    {0, 0, 1, 1, 1, 0, 0, 0, 0, 0},
}; //邻接矩阵,注意0行和0列不是有效数据

bool book[101]; //某点是否已遍历过,true则遍历过
void dfs_mar(int id)
{ //当前走到id点,邻接矩阵遍历
    cout << id << " ";
    for (int i = 1; i <= n; i++)
    {
        if (!book[i] && mar[id][i] == 1)
        { //可达且没有遍历过,就走过去
            book[i] = true;
            dfs_mar(i);
        }
    }
}

void dfs_ve(int id)
{ //当前走到id点,邻接数组遍历
    cout << id << " ";
    for (int i = 0; i < (int)ve[id].size(); i++)
    {   //id可到的点
        if (!book[ve[id][i]])
        {   //没有遍历过,视频里面忘记了,这里不需要mar[id][ve[id][i]] == 1
            book[ve[id][i]] = true;
            dfs_ve(ve[id][i]);
        }
    }
}

int main()
{
    for (int i = 1; i <= n; i++)
    { //构造邻接数组
        for (int j = 1; j <= n; j++)
        {
            if (mar[i][j] == 1)
            {
                ve[i].push_back(j);
                //注意不要ve[j].push_back(i),会放进去两次
            }
        }
    }
    book[1] = true; //从点1开始dfs遍历
    dfs_mar(1);
    cout << endl;

    fill(book, book + n + 1, false); //将book数组的boo[0]到book[n]置为false
    book[1] = true;                  //从点1开始dfs遍历
    dfs_ve(1);
    cout << endl;
    return 0;
}

2.2 BFS宽度优先搜索

也叫广度优先遍历,大概步骤如下:

  1. 定义一个存放点的容器,例如:数组、栈、队列
  2. 选取一个点放入容器(其实应该是不放入,因为执行这一步后需要取出,但是编码时为方便,需要放入),查看该点可到达的所有点,将这些点放入容器
  3. 按照一定规则,从容器中取出一个点,查看该点可到达的、且没有进入过容器的点,将这些点放入容器
  4. 重复第3步,直到容器为空(或所有点都曾取出过容器)

取出容器的顺序(如果是队列,进入容器的顺序也是,因为先进先出),就是遍历顺序

一般(甚至必须)使用队列,BFS最大的应用是分支限界算法

广搜优缺点

优点

  • 对于解决最短或最少问题特别有效,而且寻找深度小
  • 每个结点只访问一遍,结点总是以最短路径被访问,所以第二次路径确定不会比第一次短

缺点

  • 内存耗费量大(需要开大量的数组单元用来存储状态)

使用场景:计算网络数据链路层的最短跳数,走迷宫的最短路径

void bfs_mar(int id)
{ //从id点开始遍历,邻接矩阵存储
    queue qu;
    qu.push(id);                //放入第一个点
    bool book[n + 1] = {false}; //点是否进入过容器,一共n个点,编号从1开始
    book[id] = true;
    while (qu.size() > 0)
    {
        int t = qu.front(); //按照先进先出的规则,取出点
        qu.pop();
        cout << t << " "; //点出容器的顺序,就是遍历的顺序
        for (int i = 1; i <= n; i++)
        {
            if (mar[t][i] == 1 && !book[i])
            { //t可达i点,且i点没有进入过容器
                qu.push(i);
                book[i] = true;
            }
        }
    }
}

void bfs_ve(int id)
{ //从id点开始遍历,邻接表存储(一般来说更高效)
    queue qu;
    qu.push(id);
    bool book[n + 1] = {false}; //点是否进入过容器,一共n个点,编号从1开始
    book[id] = true;
    while (qu.size() > 0)
    {
        int t = qu.front(); //按照先进先出的规则,取出点
        qu.pop();
        cout << t << " "; //点出容器的顺序,就是遍历的顺序
        for (int i = 0; i < (int)ve[t].size(); i++)
        { //唯一的不同
            if (!book[ve[t][i]])
            { //可达且没有进入过容器
                qu.push(ve[t][i]);
                book[ve[t][i]] = true;
            }
        }
    }
}

2.3 DFS和BFS总结

2.3.1 总结

DFS和BFS是一种算法策略,不仅仅可应用于图的遍历

整体来看,DFS的使用情况多于BFS,BFS大多使用队列作为容器

对于天梯赛或是pat考试,DFS多用于模拟树的问题,例如下面这样:

  • 功夫传人
  • 病毒溯源

在DFS遍历中,判断每个结点的特性进行记录,年年必考

DFS的最广泛使用是回溯算法(百度下八皇后问题),BFS的最广泛使用是分支限界算法(其实是暴力算法,学校会教,可百度了解)

其他的竞赛或考试(csp、蓝桥什么的),则是以回溯和分支限界更多,基本没有模拟题了。这两个的思想理解了,万物皆可暴力,但在算法竞赛中不得分

2.3.2 DFS计算全排列

DFS的应用有很多,参考《啊哈算法》,以全排列为一个例子

3的全排列为:123、132、213、231、312、321

相当于有3个瓶子,每个瓶子放入1、2、3中的一个数,输出所有放置的顺序

#include 

using namespace std;

int n;         //求n的全排列
int box[10];   //存放数的瓶子
bool book[10]; //某数是否已经放入瓶子,true则放入了

void dfs(int step)
{ //放置第step个瓶子
    if (step == n + 1)
    { //已经放好了n个瓶子
        for (int i = 1; i <= n; i++)
        {
            cout << box[i];
        }
        cout << endl;
        return;
    }
    for (int i = 1; i <= n; i++)
    {
        if (!book[i])
        {                  //i没有放入
            box[step] = i; //把i放入第step个瓶子
            book[i] = true;
            dfs(step + 1);
            book[i] = false; //理解为什么重置为false
        }
    }
}

int main()
{
    n = 3;
    dfs(1);
    return 0;
}
#include 
#include 
using namespace std;
int main()
{
    int book[3] = {1, 2, 3};
    do
    {

        for (int i = 0; i < 3; i++)
        {
            cout << book[i];
        }
        cout << endl;
    } while (next_permutation(book, book + 3));
    return 0;
}

2.3.3 BFS计算岛屿面积(LC的绝地求生)

BFS的应用有很多,同样参考《啊哈算法》,以岛屿面积为一个例子。用这周LC绝地求生的题目当作样例

以一个点向四周拓展,遍历到1就记录下来,具体看代码:

#include 
#include 

using namespace std;

struct Node
{          //每个点
    int x; //行数
    int y; //列数
};

int main()
{
    int n = 6;
    int sx = 5, sy = 6; //起点
    int mar[n + 1][n + 1] = {
        {0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 1, 0},
        {0, 1, 0, 0, 1, 1, 0},
        {0, 0, 0, 1, 0, 1, 1},
        {0, 1, 1, 0, 1, 0, 1},
        {0, 0, 1, 1, 1, 0, 1},
        {0, 1, 1, 0, 1, 1, 1}}; //题目的样例,注意0行和0列不是有效数据
    //mar[i][j]为2,表示该点已经遍历过
    queue qu;
    qu.push({sx, sy});                                   //放入起点
    mar[sx][sy] = 2;                                     //别忘了设初始点为走过
    int ret = 1;                                         //陆地面积
    int next[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; //稍后解释
    while (qu.size() > 0)
    {
        Node t = qu.front();
        qu.pop();
        for (int i = 0; i < 4; i++)
        { //向四面走去
            //利用next计算下一步的位置
            int x = t.x + next[i][0]; //下一步可到达的行
            int y = t.y + next[i][1]; //下一步可到达的列
            if (x < 1 || x > n || y < 1 || y > n)
            { //该点越界
                continue;
            }
            else if (mar[x][y] == 1)
            {                  //该点是没有走过的陆地
                mar[x][y] = 2; //设为走过
                ret++;
                qu.push({x, y});
            }
        }
    }
    cout << ret << endl;
    return 0;
}

其实,这种在矩阵中,点1表示可走,0表示不可走的地图,叫做栅格地图(好像是)

这个是二维地图,可以尝试下三维的:肿瘤诊断

BFS还可以计算栅格地图中两点是否可达(迷宫问题)、两点的最短距离,可以思考下怎么实现

3、最短路

3.1 单源最短路径dijkstra

迪杰斯特拉算法计算带权图中,某一点距离其余点的最短路径(单源最短路径)

将图中的点看作两部分,一部分是已经找到了最短路的点集合A,另一部分是没有找到最短路的点集合B。步骤如下:

  1. 初始化其余点到点s的距离(邻接矩阵的s行,放入一维数组dis)
  2. 在B集合中寻找距离s最近的点p(dis[p]最小),将p放入集合A(s到p的最短路已经求出来了,为dis[p])
  3. 查看点p可到达的点,试探是否能够通过点p缩短它们和源点s的距离(dis[i]与dis[p]+mar[p][i])
  4. 重复步骤2和3,直到B集合为空

如果dis[i]>dis[p]+mar[p][i],说明源点到达点i的最短路径上的最后一个点为p,依据这一点记录路径

  • 要求最短路径多少条 counts[s] = 1
  • 要求边数最少的最短路 可将边数看成value = 1 counts[s] = 0
#include 

using namespace std;
#define F 9999999

int n = 7;
int mar[101][101] = {
    {0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 2, 5, F, F, F, F},
    {0, 2, 0, 2, 4, 6, F, F},
    {0, 5, 2, 0, 1, F, 3, F},
    {0, F, 4, 1, 0, 4, 1, 4},
    {0, F, 6, F, 4, 0, F, 1},
    {0, F, F, 3, 1, F, 0, 2},
    {0, F, F, F, 4, 1, 2, 0}}; //0行和0列无效,F表示无穷大(根据题目设置,一定要够大,但别越界。例如:0x3f3f3f3f)

void dijkstra(int s)
{ //计算s到其他点的最小距离
    int dis[n + 1];
    for (int i = 1; i <= n; i++)
    { //初始化距离
        dis[i] = mar[s][i];
    }
    bool book[n + 1] = {false}; //点是否在A集合,true则在
    //book[s] = true;             //源点放入A集合 ???
    int pre[n + 1] = {0};       //路径点,pre[i]表示s到i的最短路上的最后一个点
    for (int i = 1; i <= n ; i++)
    {
        int p  = -1;
        int cnt = F;
        for (int j = 1; j <= n; j++)
        { //寻找当前距离源点最近的点p
            if (!book[j] && dis[j] < cnt)
            {
                p = j;
                cnt = dis[j];
            }
        }
        if (p == -1)
            break;
        //非连通图一定要判断
        book[p] = true; //点p放入A集合
        for (int j = 1; j <= n; j++)
        {
            //其实!book[j]可有可无
            if (!book[j] && dis[j] > dis[p] + mar[p][j])
            {
                dis[j] = dis[p] + mar[p][j]; //更新
                pre[j] = p;                  //s先到p,再到j的路径最短
            }
        }
    }
    for (int i = 1; i <= n; i++)
    {
        cout << i << ": " << dis[i] << endl; //输出最短路径长度
        //输出路径,注意是倒着的
        int t = pre[i];
        string ret = to_string(i);
        //cout << i << "<--";
        while (t != 0)
        {
            //cout << t << "<--";
            ret = to_string(t) + "-->" + ret; //正过来
            t = pre[t];
        }
        //cout << s << endl;
        ret = to_string(s) + "-->" + ret;
        cout << ret << endl
             << endl;
    }
}

int main()
{
    dijkstra(1);
    return 0;
}

该算法不能处理负权边,可百度学习bellman-ford算法(没有涉及过题目)

3.2 旅游规划

在保证路径最短得情况下,花费最短,对算法进行改动即可。具体看代码:

#include 

using namespace std;
#define MAX_SIZE 501 //最大点数
#define INF 9999999  //无穷,相对500足够大

int n, e, s, d;
int mar1[MAX_SIZE][MAX_SIZE]; //距离
int mar2[MAX_SIZE][MAX_SIZE]; //收费

void dijkstra(int s)
{                     //计算s到其他点的最小距离
    int dis[n + 1];   //最短路径
    int price[n + 1]; //最短路基础上的最小花费,即price[i]表示在s到达i最短路径上的花费
    for (int i = 0; i < n; i++)
    { //初始化
        dis[i] = mar1[s][i];
        price[i] = mar2[s][i];
    }
    bool book[n + 1] = {false};
    book[s] = true; //源点放入A集合
    for (int i = 1; i <= n - 1; i++)
    {
        int p;
        int cnt = INF;
        for (int j = 0; j < n; j++)
        { //寻找当前距离源点最近的点p
            if (!book[j] && dis[j] < cnt)
            {
                p = j;
                cnt = dis[j];
            }
        }
        book[p] = true; //点p放入A集合
        for (int j = 0; j < n; j++)
        {
            //其实!book[j]条件可有可无
            if (!book[j] && dis[j] > dis[p] + mar1[p][j])
            {
                dis[j] = dis[p] + mar1[p][j];     //更新路径长度
                price[j] = price[p] + mar2[p][j]; //路径最短的基础上更新花费
            }
            else if (!book[j] && dis[j] == dis[p] + mar1[p][j] && price[j] > price[p] + mar2[p][j])
            { //路径长度相同,但走点p到j可以花费更小
                price[j] = price[p] + mar2[p][j];
            }
        }
    }
    cout << dis[d] << " " << price[d] << endl;
}

void init()
{ //初始化邻接矩阵
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            if (i == j)
            { //自己到自己是0
                mar1[i][j] = 0;
                mar2[i][j] = 0;
            }
            else
            {
                mar1[i][j] = INF;
                mar2[i][j] = INF;
            }
        }
    }
}

int main()
{
    cin >> n >> e >> s >> d;
    init(); //注意输入n后,输入e条边前初始化
    for (int i = 1; i <= e; i++)
    {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        mar1[a][b] = mar1[b][a] = c; //无向图
        mar2[a][b] = mar2[b][a] = d;
    }
    dijkstra(s);
    return 0;
}

本题没有要求输出路径经过的点,不需要pre数组

这种在保证路径最短条件下,实现其余某项条件的最短路径题很重要,有的还会要求输出路径经过的点,基本都是使用迪杰斯特拉算法解题,天梯赛和PAT考试必须掌握

如下为类似题:

  • 城市间紧急救援
  • 垃圾箱分类

3.3 任意两点间最短路径floyld

佛洛依德算法计算带权图中,任意两点间的最短路径

依次以图中的每个点为跳板,尝试用该点缩短每个点和其余点的距离,具体看代码理解,很短

#include 

using namespace std;
#define F 9999999

int n = 7;
int mar[101][101] = {
    {0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 2, 5, F, F, F, F},
    {0, 2, 0, 2, 4, 6, F, F},
    {0, 5, 2, 0, 1, F, 3, F},
    {0, F, 4, 1, 0, 4, 1, 4},
    {0, F, 6, F, 4, 0, F, 1},
    {0, F, F, 3, 1, F, 0, 2},
    {0, F, F, F, 4, 1, 2, 0}}; //0行和0列无效,F表示无穷大(根据题目设置,一定要够大,但别越界。例如:0x3f3f3f3f)

void floyld()
{
    for (int k = 1; k <= n; k++)
        for (int a = 1; a <= n; a++)
            for (int b = 1; b <= n; b++)
                if (mar[a][b] > mar[a][k] + mar[k][b])
                    mar[a][b] = mar[a][k] + mar[k][b]; //用点k缩短a与b的距离
}

int main()
{
    floyld();
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            cout << mar[i][j] << " "; //mar[i][j]即为i到j的最短距离
        }
        cout << endl;
    }
    return 0;
}

社交网络图中结点的“重要性”计算

4、prim最小生成树(畅通工程)

普利姆算法计算带权图的最小生成树

总体来说:从一点开始创建树,不断选取距离树最近的点逐渐把整个树扩大,直到覆盖所有点

所以,需要一个数组存储每个点距离树的距离。具体步骤如下:

  1. 选定一个点s(从该点开始构建树),初始化其余点到点s的距离(邻接矩阵的s行,放入数组dis)
  2. 选取不在树中,且距离树最近的点p(dis[p]最小)
  3. 查看点p可到达的点,尝试通过点p缩短它们和树的距离(dis[i]和mar[p][i])
  4. 重复步骤2、3,直到树覆盖所有点
void prim(int s)
{ //从s点开始构造最小生成树,邻接矩阵存储图
    int dis[n + 1];
    for (int i = 1; i <= n; i++)
    { //初始化点到树的距离
        dis[i] = mar[s][i];
    }
    bool book[n + 1] = {false}; //点是否在树中,true则在
    book[s] = true;             //起始树中只有s一个点
    int ret = 0;                //最小的总权重
    for (int i = 1; i <= n - 1; i++)
    {               //每一次循环将一个点放进树中
        int p = -1; //初值-1
        int cnt = INF;
        for (int j = 1; j <= n; j++)
        { //寻找不在树中,且距离树最近的点
            if (!book[j] && dis[j] < cnt)
            {
                p = j;
                cnt = dis[j];
            }
        }
        if (p == -1)
        { //如果p的初值没变,说明图不连通
            cout << "Impossible" << endl;
            return;
        }
        book[p] = true; //点p进入树
        ret += dis[p];  //加上总权重
        dis[p] = 0;     //其实可有可无
        for (int j = 1; j <= n; j++)
        { //用点p尝试缩短其余点到树的距离
            if (!book[j] && dis[j] > mar[p][j])
            {
                dis[j] = mar[p][j];
            }
        }
    }
    cout << ret << endl;
}

和dijkstra很像,都是贪心思想的应用

kruskal算法有一个难点,如何编程判断图中是否存在回路,一般使用并查集判断,下周(最后一周)会介绍

5、图的其他知识

图的知识很广,如:着色、TSP、欧拉路、拓扑排序、关键路径等,学校都会教

算法题目如何判断出是否使用图来解答是需要做题锻炼的

1、树基本概念

区分清树的一些概念名词

  • 父结点、孩子结点、兄弟结点
  • 根节点、叶子结点
  • 高度(层数)

最常用的是二叉树

  • 完全二叉树、满二叉树(完美二叉树)
  • 左孩子、右孩子

2、二叉树存储

2.1 链式

类似链表结点结构体,一部分存储结点表示的数据,一部分存储其相关结点的地址(指针)

相关节点可以是孩子结点、父节点、兄弟结点等,看实际题目,自己设置方便答题

struct Node {
    int data;//数据
    Node* leftChild;//左孩子地址,没有则为NULL
    Node* rightChild;//右孩子地址,没有则为NULL
};//孩子结点表示法,一般这样足够用

struct Node {
    int data;//数据
    Node* father;//加一个父结点地址
    Node* leftChild;
    Node* rightChild;
};

2.2 一维数组

t假设一维数组tree存储了一个二叉树,它满足如下规则:

  1. tree[1]存储了树的根,即1是树根节点的索引
  2. tree[i/2]是tree[i]的父结点,即i索引位置结点的父结点索引是i/2
  3. tree[i*2]是tree[i]的左孩子结点,即i索引位置结点的左孩子结点索引是i*2
  4. tree[i*2+1]是tree[i]的右孩子结点,即i索引位置结点的右孩子结点索引是i*2+1

使用数组存储,会浪费一些空间,tree[0]必定会浪费掉

如果是完全二叉树,数组容量开辟为结点数加1,仅仅会浪费tree[0]的空间

(涉及完全二叉树的题目,大概率使用数组存储好做题)

3、二叉树遍历

3.1 层序遍历

从上到下逐层,每层从左到右的遍历

完全二叉树的层序遍历顺序,就是其使用一维数组存储的结点顺序(⭐⭐⭐⭐⭐)

从根节点,对树执行一次队列容器的bfs

struct Node {
    int data;
    Node* leftChild;
    Node* rightChild;
};

void levelOrder(Node* root) { //链式存储,从根节点root,开始层序遍历
    queue qu;//存储结点地址
    qu.push(root);
    while(!qu.empty()) {
        Node* t = qu.front();
        qu.pop();
        //cout << t->data << endl;//输出结点值,看题目需要,确定每个结点需要做什么
        if(t->leftChild) {//如果有左孩子
            qu.push(t->leftChild);
        }
        if(t->rightChild) {//如果有右孩子
            qu.push(t->rightChild);
        }
    }
}

一维数组存储的遍历如下:

int tree[101];//tree[i]为-1表示当前位置无结点

void levelOrder() { //数组存储,开始层序遍历
    queue qu;//存储结点的索引
    qu.push(1);//1号索引肯定是树的根
    while(!qu.empty()) {
        int t = qu.front();
        qu.pop();
        //看题目需要,确定每个结点需要做什么
        if(tree[t * 2] != -1) { //如果有左孩子
            qu.push(t * 2);
        }
        if(tree[t * 2 + 1] != -1) { //如果有右孩子
            qu.push(t * 2 + 1);
        }
    }
}

3.2 三种递归遍历

需要理解树的递归定义,各自的遍历规则如下:

  • 前序遍历:根结点–>左子树–>右子树
  • 中序遍历:左子树->根节点–>右子树
  • 后序遍历:左子树–>右子树–>根节点

慢慢理解,只可意会不可言传。代码很好记,背下来慢慢理解

struct Node {
    int data;
    Node* leftChild;
    Node* rightChild;
};

void preOrder(Node* root) { //链式存储,先序遍历
    if(root) {
        cout << root->data << endl;//输出根
        preOrder(root->leftChild);//遍历左子树
        preOrder(root->rightChild);//遍历右子树
    }
}

void inOrder(Node* root) { //链式存储,中序遍历
    if(root) {
        inOrder(root->leftChild);//遍历左子树
        cout << root->data << endl;//输出根
        inOrder(root->rightChild);//遍历右子树
    }
}

void lastOrder(Node* root) { //链式存储,后序遍历
    if(root) {
        lastOrder(root->leftChild);//遍历左子树
        lastOrder(root->rightChild);//遍历右子树
        cout << root->data << endl;//输出根
    }
}

有迭代的实现方法,学校会教

3.3 举例应用

考察二叉树遍历的题目,要么不会直接说输出什么遍历结果,需要自己分析使用什么遍历方式;要么告诉你输出某种遍历的结果,但是这样题目往往就困难了

3.3.1 (7-1 列出叶结点)

题目说从上到下,从左到右的输出叶子结点,刚好符合bfs层序遍历的结点遍历顺序

存储好二叉树后,层序遍历即可,将叶子结点存储准备输出

注意先确定根节点是哪个

3.3.2 (7-2 完全二叉树的层序遍历)

完全二叉树,使用数组存储

在后序遍历中输入,存储进数组

完全二叉树的层序遍历顺序,就是其使用一维数组存储结点的顺序。最后输出数组即可

4、并查集

用于解决多个集合的合并问题,将有相同元素的多个集合合并在一起

初始时将每个元素看作一个集合,根据条件不断合并

每个集合选定一个头目,如果元素a和元素b的头目相同,则它们在一个集合中。依据条件全部合并完成后,有几个集合头目,就有几个集合

合并原则:

  • 以左为尊:给定a和b在一起,则b跟随a
#include 
// 万能头文件会重名
using namespace std;
#define MAX_SIZE 10001

int f[MAX_SIZE + 1];//存储元素的首领

void init() {
    for(int i = 1; i <= MAX_SIZE; i++) { //初始化,元素自己就是一个集合
        f[i] = i;
    }
}

int query(int i) {//查询i元素的头目
    if(f[i] != i) {
        f[i] = query(f[i]);//可能错认首领(路径压缩)
    }
    return f[i];
}

void fmerge(int a, int b) {//合并a和b
    int t1 = query(a);
    int t2 = query(b);
    if(t1 != t2) {//两元素当前不在同一集合,进行合并
        f[t2] = t1;//以左为尊
    }
}

int main() {
    int n,m;
    cin >> n >> m;//n对线索,m个元素(1~m编号)
    init();//初始化并查集
    for(int i = 1; i <= n; i++) {
        int a, b;
        cin >> a >> b;//a和b在一起
        fmerge(a, b);
    }
    int ret = 0;
    for(int i = 1; i <= m; i++) { 
        f[i] = query(f[i]);//统一首领
        if(f[i] == i) {
            ret++;
        }
    }
    cout << ret << endl;//最终集合数
    return 0;
}

可用于判断图中是否存在回路,百度实现kruskal算法

5、完全二叉树——堆

满足如下规则的完全二叉树:

  • 最大堆(大根堆):父结点数据大于等于孩子结点
  • 最小堆(小根堆):父结点数据小于等于孩子结点

于是,小根堆中的根节点是最小数据的点,大根堆中的根节点是最大数据的点

使用一维数组存储即可

  1. 将一颗完全二叉树转变为堆
  2. 堆中插入结点
  3. 堆中删除结点(堆只能删除堆顶,即根结点)

如下代码以最大堆为例:

#include 

using namespace std;

int heap[101];
int heap_size;//1~heap_size

void siftdown(int i) {//向下寻找正确位置
    int child = i * 2;
    while (child <= heap_size) {
        if (child + 1 <= heap_size && heap[child] < heap[child + 1]) { //有右孩子且右孩子更大
            child++;//变为右孩子
        }
        if (heap[i] >= heap[child]) {
            return;
        }
        swap(heap[i], heap[child]);
        i = child;
        child = i * 2;
    }
}

void push_data(int data) {//向堆内插入结点
    int current = ++heap_size;
    heap[current] = data;//先放在最底部
    while (current != 1 && heap[current / 2] < heap[current]) {//向上寻找正确位置
        swap(heap[current], heap[current / 2]);
        current = current / 2;
    }
}

void pop_data() {//移除堆顶
    int t=heap[1];
    heap[1] = heap[heap_size--];
    siftdown(1);
}

void look_heap() {//遍历
    for (int i = 1; i <= heap_size; i++) {
        cout << heap[i] << " ";
    }
    cout << endl;
}

int main() {
    cout << "输入堆容量:";
    cin >> heap_size;
    cout << "输入数据:";
    for (int i = 1; i <= heap_size; i++) {
        cin >> heap[i];
    }
    for (int i = heap_size / 2; i >= 1; i--) {//完全二叉树转为大根堆
        siftdown(i);//向下调整
    }
    cout << "-----------最大堆-----------" << endl;
    look_heap();
    return 0;
}

当多次对一个序列插入数据、修改数据,且需要多次计算序列的极值,堆可用于提高效率

可以去了解make_heap、push_heap、pop_heap函数和priority_queue容器,以及堆排序(nlogn)

6、其他

6.1 树状题目的DFS求解(7-5 病毒溯源)

把给定的样例画画,树结构可以看成从根结点向下的有向图,就可以使用邻接数组存储

使用dfs从根节点遍历树,在每个结点处进行相应计算即可

注意确定根节点是哪个,大部分题目在这里设坑,根节点不一定总是编号最小的点

病毒溯源代码如下:

#include 
#include 

using namespace std;
#define MAX_SIZE 10001

vector> ve(MAX_SIZE);//ve[i]存储i的孩子结点
vector ret;

void dfs(int id, vector &road) {//当前遍历至id点,引用为提高效率
    if(ve[id].size() == 0) {//当前结点是叶子结点,走到头了
        if(road.size() > ret.size()) {//更长则直接赋值
            ret = road;
        } else if(road.size() == ret.size() && road < ret) { //相等则选小的
            ret = road;
        }
        return;
    }
    for(int i = 0; i < (int)ve[id].size(); i++) {
        road.push_back(ve[id][i]);
        dfs(ve[id][i], road);
        road.pop_back();
    }
}

int main() {
    int n;
    cin >> n;
    bool book[n] = {false};//结点是否有父亲,true则有
    for(int i = 0; i < n; i++) {
        int k;
        cin >> k;
        while(k--) {
            int id;
            cin >> id;
            ve[i].push_back(id);
            book[id] = true;
        }
    }
    int root;
    for(int i = 0; i < n; i++) {//寻找根结点
        if(!book[i]) {
            root = i;
            break;
        }
    }
    vector ve;
    ve.push_back(root);
    dfs(root, ve);
    cout << ret.size() << endl;
    for(int i = 1; i <= (int)ret.size(); i++) {
        cout << ret[i - 1];
        cout << (i == (int)ret.size() ? '\n' : ' ');
    }
    return 0;
}

这种树状题目DFS解法一定要熟悉,在天梯赛、PAT考试中可以说是必考题,也能方便以后学习算法课

6.2 二叉树还原(7-7 树的遍历)

  • 通过先序遍历和中序遍历,可还原一棵二叉树
  • 通过后序遍历和中序遍历,可还原一棵二叉树

本周最后一题就是后序和中序还原二叉树,刚开始接触很难理解,建议彻底理解了三种递归遍历后再尝试,我当时直接百度的

百度完了,了解了还原方法后,可以看看我个人的题解如下:(后序和中序还原二叉树)

#include 
#include 

using namespace std;
#define MAX_SIZE 31

int n;
int postOrder[MAX_SIZE];//后序
int inOrder[MAX_SIZE];//中序
int height = 0;//树的层数(高度)
vector level_node[MAX_SIZE];//每一层的结点

void build_tree(int root, int left, int right, int level) {//参数读代码了解含义
    if (left > right) {
        return;
    }
    int t = left;
    while (t < right && inOrder[t] != postOrder[root]) {//使用中序拆分左右子树
        t++;
    }
    level_node[level].push_back(postOrder[root]);//该层加入结点
    if (level > height) {
        height = level;
    }
    build_tree(root - (right - t + 1), left, t - 1, level + 1);//左子树
    build_tree(root - 1, t + 1, right, level + 1);//右子树
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {//索引从1开始
        cin >> postOrder[i];
    }
    for (int i = 1; i <= n; i++) {//索引从1开始
        cin >> inOrder[i];
    }
    //后序遍历的最后一个,是树的根
    build_tree(n, 1, n, 1);
    vector ret;
    for (int i = 1; i <= height; i++) {
        for (int j = 0; j < (int)level_node[i].size(); j++) {
            ret.push_back(level_node[i][j]);//把结果放入ret,省得空格不好判断
        }
    }
    for(int i = 1; i <= n; i++) {
        cout << ret[i - 1];
        cout << (i == n ? '\n' : ' ');
    }
    return 0;
}

也有前序和中序还原二叉树的题,还原二叉树

如下是我个人题解:

#include 
#include 
using namespace std;
int const MAX_SIZE = 51;
int n;
int preOrder[MAX_SIZE];
int inOrder[MAX_SIZE];
int height = 0;
void build_tree(int root, int left, int right, int level)
{
    if (left > right)
    {
        return;
    }
    int temp = left;
    while (temp < right && inOrder[temp] != preOrder[root])
    {
        temp++;
    }
    if (level >= height)
    {
        height = level;
    }
    build_tree(root + 1, left, temp - 1, level + 1);
    build_tree(root + (temp - left + 1), temp + 1, right, level + 1);
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        char ch;
        cin >> ch;
        preOrder[i] = (int)ch;
    }
    for (int i = 1; i <= n; i++)
    {
        char ch;
        cin >> ch;
        inOrder[i] = (int)ch;
    }
    build_tree(1, 1, n, 1);
    cout << height << endl;
    return 0;
}
中序和后序推前序
#include 
using namespace std;
int post[] = {3, 4, 2, 6, 5, 1};//后序
int in[] = {3, 2, 4, 1, 6, 5};//中序
void pre(int root, int start, int end)//后序中root的位置 中序中左右子树的起止区间
{   
    //个数为0
    if (start > end)
        return;
    int i = start;
    while (i < end && in[i] != post[root])
        i++;
    printf("%d ", post[root]);
    pre(root - 1 - end + i, start, i - 1);
    pre(root - 1, i + 1, end);
}

int main()
{
    pre(5, 0, 5);
    return 0;
}
前序和中序推后序
#include 
using namespace std;
int pre[] = {1, 2, 3, 4, 5, 6};
int in[] = {3, 2, 4, 1, 6, 5};
void post(int root, int start, int end) {
    if(start > end) 
        return ;
    int i = start;
    while(i < end && in[i] != pre[root]) i++;
    post(root + 1, start, i - 1);
    post(root + 1 + i - start, i + 1, end);
    printf("%d ", pre[root]);
}

int main() {
    post(0, 0, 5);
    return 0;
}

//法二
TreeNode* buildTree(int root, int start, int end) {
    if(start > end) return NULL;
    int i = start;
    while(i < end && in[i] != pre[root]) i++;
    TreeNode* t = new TreeNode();
    t->left = buildTree(root + 1, start, i - 1);
    t->right = buildTree(root + 1 + i - start, i + 1, end);
    t->data = pre[root];
    return t;
}

你可能感兴趣的:(数据结构和算法,数据结构,c++)