写给仅有C语言基础的std::list教程

2021-03-18   


写给仅有C语言基础的std::list教程

用std::list代替手写链表

一. 说明

看标题,写给无C++基础的读者
很多地方,我也知道该用lambda表达式,该用for_each,但是毕竟是写给无C++基础的,这时候谈什么匿名函数为时过早
作者本身是小白,很多表述也只是为了无C++基础的好理解,请大佬请勿喷

链表写起来麻烦(当然如果还不熟练建议多练习)
其实在C++的STL中有个容器叫list,本质上是一个双向链表
因此可以使用使用list完成链表的题目
不造轮子,省时省力啊!

二. 引子

1. 关于编译器

因为是使用C++,要使用c++的编译器
选择 g++ 或者 clang++ 都可以

2. 关于C++

C++并不是什么高大上的东西
编译器选择C++,除了使用list以外,其他代码可以完全写成C语言风格
因此这里并不存在门槛问题

3. 关于头文件

因为list包含在list头文件中
后面用到的find函数包含在algorithm头文件中
因此使用的时候应该在开头写上

#include <list>
#include <algorithm>
// 至于这一个,每次直接这样写就好了
using namespace std;

4. 介绍迭代器

迭代器这个名字听起来很高大上
不要被这个高大上的名字吓坏了
对于迭代器 目前理解成是一个指针就好了
就记住这句话 “迭代器先理解成指针

指针是可以进行++操作的
++操作的结果是,指针会移向目前保存的这个地址的下一个地址
比如一个int* 的指针

int ia[10] = {0};
// p指向ia的第一个元素 下标为0
int *p = ia;
// p被后移一位 指向ia的第二个元素 下标为1
++p;

迭代器也可以执行同样的操作
假设it是一个迭代器 指向某个元素

// it由目前指向的这个元素后移一位 指向了下一个元素
it++;

–操作同样类比

三. list的声明和定义(即创建一个链表)

// list<链表元素的类型> 链表名称;
// 超简单 一行搞定
list<int> l;
list<char> cl;
list<double> dl;

四. list的"增"操作

list有两个成员函数 对应链表操作中的"头插" 和 “尾插”
list.push_back() 往链表尾部插入元素
list.push_front() 往链表头部插入元素

// int型的链表
list<int> l;
// 往链表尾部添加元素123
l.push_back(123);
// 往链表头部添加元素233
l.push_front(233);

五. 用迭代器遍历list

再来讲迭代器
我们刚刚说到迭代器可以类比指针
假设现在一个链表有 0-910个元素

// 建立一个int链表 里面有0-9 共10个元素

list<int> l;

for (int cnt = 0; cnt != 10; ++cnt)
{
  // 在尾部插入
  l.push_back(cnt);
}

我们可以获取list的头迭代器尾后迭代器

头迭代器 : 指向链表第一个元素,在上面那个链表中,即指向1

尾后迭代器 : 指向链表的最后一个元素的后一个元素,在上那个链表中,即指向10的后一个元素
特别要注意的:尾后迭代器没有实际意义 只用于判断

下面是获得头迭代器和尾后迭代器的操作

// 这里的auto跟int char一样,是数据类型
// auto类型让编译器自动帮忙猜测这个变量的类型
// 由于迭代器的类型写起来非常麻烦 因此大多使用auto声明迭代器
auto ibegin = l.begin();
auto iend = l.end();

上面说了迭代器可以类比指针,也可以进行解引用操作获得值

// 链表 0 1 2 3 4 5 6 7 8 9
// ibegin指向链表的第一个元素0
printf("%d", *ibegin); // ibegin指向0 因此这里输出0

迭代器同样也可以像指针一样使用

*ibegin = 2; // 链表的第一个元素由1变成2

迭代器可以执行++操作

// 链表 0 1 2 3 4 5 6 7 8 9
// ibegin原来指向链表的第一个元素0
// 这时候ibegin后移一位
// 现在指向0的后一位 1
++ibegin;
printf("%d", *ibegin); // 输出1

用迭代器遍历链表

// it从指向链表的第一个元素开始
// 到指向链表的最后一个元素的后一个元素停止
// **还是要说明** .l.end()是尾后迭代器
// 链表 0 1 2 3 4 5 6 7 8 9
// l.end()指向了9的后一个元素
// 这里使用 != 就是让它遍历到9的后一个元素就停止
// 每次循环后,it就向后移动一个,指向后一个元素
for (auto it = l.begin(); it != l.end(); ++it)
{
  printf("%d ", *it);
}
// 输出结果 0 1 2 3 4 5 6 7 8 9

六. 获得当前迭代器的 前一个元素 和 后一个元素 (相邻元素)

链表中经常有访问元素next指针的操作
而list也可以轻松思想这个操作
假设还是刚刚那个链表 0 1 2 3 4 5 6 7 8 9
it是个迭代器,指向元素5

// it2指向5的前一个元素4
auto it2 = prev(it);
// it3指向5的后一个元素6
auto it3 = next(it);
// 输出
// ====这里漏了*====
printf("%d %d", *it2, *it3); // 输出结果4 6

七. list的"查"操作

list除了用刚刚上述方法遍历查找以外
提供了好用的查找函数
还是刚刚那个链表 0 1 2 3 4 5 6 7 8 9
要查找 元素 5
当然可以使用下面的这个操作

for (auto it = l.begin(); it != l.end(); ++it)
{
  if(*it == 5)
  {
      printf("我找到5了");
  }
}

但是还有更简洁的查找方式

// find 查找函数  
// l.begin() l.end()表示从l开头查找到结尾
// 后面的5就表示我要查找=5的元素
auto it = find(l.begin(), l.end(), 5);
// **还是要说明** .l.end()是尾后迭代器
// 它指向9后一个元素
// 而9后一个元素其实不存在
// 所以如果查找到的迭代器等于end 那么就表示这个数组中并没有5
if(it != c.end())
{
  printf("我找到5了");
} 

八. list的"改"操作

it是迭代器,可以通过it轻松修改元素的值

// 找到list中的5 并且修改成6
auto it = find(l.begin(), l.end(), 5);
if(it != c.end())
{
  *it = 6;
} 

八. list的"插"操作

list提供了insert() 函数
通过这个函数可以很方便的进行插入
还是刚刚那个链表 0 1 2 3 4 5 6 7 8 9
我想要在6的前面插入10
我可以这么写:

// 找到6的迭代器
auto it = find(l.begin(), l.end(), 6);
// 在6这个元素的前面插入10
l.insert(it, 10);

九. list的"删"操作

说删操作前
先回顾一下qsort排序函数
qsort中有个参数传入一个函数
这个函数的返回值决定了当前这个元素排在前面还是后面
同样的,list提供的remove_if操作也需要传入一个函数
当这个函数返回true时,这个元素将被删除
当这个函数返回false时,这个元素不删除

// 函数的参数n将被传入当前元素的值
bool to_remove(int n)
{
   if (n == 6)
   {
       // 返回true 表示将被删除
       return true;
   }
   else
       // 返回false 表示不会被删除
       return false;
}

应该不难理解,这个函数只有当元素值=6时,才会返回true
也就表面 要删除值为6的元素可以这么写:

// to_remove 是 上面写的函数
l.remove_if(to_remove);

十. 附上我写这个的时候的代码

#include <list>
#include <algorithm>

using namespace std;

bool to_remove(int n)
{
    if (n == 1)
    {
        return true;
    }
    else
        return false;
}

int main()
{
    // 创建
    list<int> l;
    /* 增 */
    // 链表最基本的操作 头插和尾插
    for (int cnt = 0; cnt != 10; ++cnt)
    {
        l.push_back(cnt);
    }
    // 遍历输出链表
    for (auto it = l.begin(); it != l.end(); ++it)
    {
        printf("%d ", *it);
    }
    // 查
    auto it = find(l.begin(), l.end(), 5);
    // 改
    if (it != l.end())
    {
        *it = 6;
    }
    // 删
    l.remove_if(to_remove);

    // 插
    l.insert(it, 15);

    return 0;
}

十一. 结

很多东西其实讲的不是很准确
毕竟作者也是初学者
希望有所帮助吧 能用上确实快
如上操作掌握了,其他STL的容器如std::vector std::array也可以很轻松上手

Q.E.D.