【挑战程序设计竞赛 | 笔记】:数据结构

48 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 29 天,点击查看活动详情

什么是数据结构

数据结构是一种在程序中系统化管理数据集合的形式。

数据结构由三部分组成:

  1. 数据集合:就是用于存放一个个数据对象的集合(比如数组、结构体等)
  2. 规则:保证数据集合按照一定规矩进行正确操作 、 管理和保存的规则
  3. 操作:包含了对数据集合进行的操作,比如插入元素、取出元素、查询元素,检查元素等

下面记录两个基本的数据结构:栈 和 队列。

栈(Stack)是一种常见的数据结构(能有效帮助我们临时保存数据)。

栈的常见操作有:

  • push(x):在栈的顶部添加元素 x
  • pop():从栈的顶部取出元素
  • isEmpty():检查栈是否为空
  • isFull():检查栈是否已满

栈的其他操作有:"引用栈顶元素"、"检查栈中是否含有指定元素"

栈的规则

  • 在一个栈中,最后进入的元素一定是最先被取出的,即后入先出
  • 按照最后进入栈的数据最后出栈的规则进行管理数据。
  • pop() 取出的元素是最后一次被 push(x) 的元素

队列

队列(Queue)也是一种常见的数据结构(一个等待处理的行列)。

队列的常见操作有:

  • enqueue(x):从队列末尾取出元素
  • dequeue(c):从队列开头取出元素
  • isEmpty():检查队列是否为空
  • isFull():检查队列是否已满

队列的其他操作有:"引用队头元素"、"检查队列中是否含有指定元素"

队列的规则:

  • 在一个队列中,元素必须按照抵达的先后顺序被处理
  • 最先放入的元素最先被取出,即先入先出
  • 距上一次 enqueue 时间最久的元素最先被取出

标准库的数据结构

C++库以提供标准 “模板”为主,我们可以借助标准模板库(STL)提供的高效算法来管理数据。

为了应对多种需求,STL 为用户提供了多种名为容器(Container)的类,用于管理数据集合。在创建栈、队列、动态数组等数据结构时,我们只需要定义对应的容器,然后调用相应的成员函数或者算法即可。

下面记录四种常见的容器。

stack

我们可以借助 stack 来实现栈,用它管理数据。

在使用之前,我们需要将 STL 中的 stack 包含到程序中,即:

#include<stack>

好了,现在我们来声明一个栈 s ,用于管理 int 类型的元素。

stack<int> s;

如上所示,STL 提供的 stack 是一个模板,我们需要在 <> 中指定栈容器要管理的数据类型。

有了栈 s 后,我们可以调用其成员函数,如下所示:

  • push(x):向栈中添加元素 x
  • pop():从栈中取出元素并删除元素
  • size():返回栈的元素数
  • top():返回栈顶的元素
  • empty():判断栈是否为空

下面举例了栈的使用:

#include<iostream>
#include<stack>
using namespace std;
​
int main() {
    // 声明一个栈 s
    stack<int> s;
    
    // 向栈中压入三个元素:5  9  2
    cout << "向栈中分别压入元素:\n 5  9  2" << endl;
    s.push(5);
    s.push(9);
    s.push(2);
    
    // 查看栈的大小
    cout << "当前栈的大小为:" << s.size() << endl;
    
    // 查看栈顶元素
    cout << "当前栈顶元素为:" << s.top() << endl;
    
    // 从栈顶删除元素
    cout << "删除栈顶元素..." << endl;
    s.pop();
    
    // 查看新的栈顶元素
    cout << "当前新的栈顶元素为:" << s.top() << endl;
    
    // 删除两个元素
    cout << "删除两个元素" << endl;
    s.pop();
    s.pop();
    
    // 查看当前栈是否为空
    if(s.empty()) {
        cout << "栈已为空" << endl;
    } else {
        cout << "栈不为空" << endl;
    }
    
    return 0;
}

运行结果:

向栈中分别压入元素:
 5  9  2
当前栈的大小为:3
当前栈顶元素为:2
删除栈顶元素...
当前新的栈顶元素为:9
删除两个元素
栈已为空

queue

我们可以借助 queue 来实现队列,用它管理数据。

在使用之前,我们需要将 STL 中的 queue 包含到程序中,即:

#include<queue>

好了,现在我们来声明一个队列 q ,用于管理 string 类型的元素。

queue<string> q;

如上所示,STL 提供的 queue 是一个模板,我们需要在 <> 中指定栈容器要管理的数据类型。

有了队列 q 后,我们可以调用其成员函数,如下所示:

  • push(x):向队列中添加元素 x
  • pop():从队列中取出元素并删除元素
  • size():返回队列的元素数
  • front():返回队头的元素
  • empty():判断队列是否为空

下面举例了栈的使用:

#include<iostream>
#include<queue>
#include<string>
using namespace std;

int main() {
  // 声明一个队列 q 
  queue<string> q;
  
  // 向队列中添加三个元素:one two three 
  cout << "向栈中分别压入元素:\n one  two  three" << endl;
  q.push("one");
  q.push("two");
  q.push("three");
  
  // 查看队列的大小
  cout << "当前队列的大小为:" << q.size() << endl;
  
  // 查看队头元素
  cout << "当前队头元素为:" << q.front() << endl;
  
  // 从队头删除元素
  cout << "删除队头元素..." << endl;
  q.pop();
  
  // 查看新的队头元素
  cout << "当前新的队头元素为:" << q.front() << endl;
  
  // 删除两个元素
  cout << "删除两个元素" << endl;
  q.pop();
  q.pop();
  
  // 查看当前队列是否为空
  if(q.empty()) {
    cout << "队列为空" << endl;
   } else {
    cout << "队列不为空" << endl;
   }
  
  return 0;
}

运行结果:

向栈中分别压入元素:
 one  two  three
当前队列的大小为:3
当前队头元素为:one
删除队头元素...
当前新的队头元素为:two
删除两个元素
队列为空

vector

我们可以借助 vector 来实现一个动态数组,用它管理数据。

在之前,我们自己定义的数组都是定长数组,即定义完数组的大小之后,不能改变其大小。这种静态数组是只能容纳一定数量元素的数组。但是,借助 vector ,我们可以定义可变长数组,这种动态数组可以在添加元素时增加长度。

在使用动态数组之前,我们需要将 STL 中的 vector 包含到程序中,即:

#include<vector>

好了,现在我们来声明一个动态数组 v ,用于管理 double 类型的元素。

vector<double> v;

如上所示,STL 提供的 vector 是一个模板,我们需要在 <> 中指定栈容器要管理的数据类型。

在访问动态数组中的元素,无论是赋值还是写入,都与普通数组一样使用 []操作符。

有了动态数组 v 后,我们可以调用其成员函数,如下所示:

  • push_back(x):将元素 x 添加到动态数组的末尾
  • pop_back():删除动态数组的最后一个元素
  • size():返回动态数组的元素数
  • insert(p, x):在动态数组中位置为 p 处插入元素 x
  • erase(p):删除动态数组中位置为 p 的元素
  • clear():删除动态数组中的所有元素
  • begin():返回指向动态数组开头的迭代器
  • end():返回指向动态数组最后一个元素的后一个位置的迭代器

这里的迭代器可以看成一个指针。

下面举例了动态数组的使用:

#include<iostream>
#include<vector>
using namespace std;

void print(vector<double> V) {
    cout << "\n此时动态数组为:[" ;
    for(int i = 0; i<V.size(); i++) {
        cout << V[i];
        if(i != V.size()-1) cout << ", "; 
    }
    cout << "]\n\n";
}

int main() {
    // 声明一个动态数组 
    vector<double> v;
    
    // 添加三个元素
    cout << "往动态数组中添加三个元素:\n 0.12  0.34  0.56" << endl;
    v.push_back(0.12);
    v.push_back(0.34);
    v.push_back(0.56);
    print(v); 
    
    // 修改动态数组中的第三个元素 
    cout << "修改v[2]为0.78" << endl;
    v[2] = 0.78;
    print(v); 
    
    // 往数组中第三个位置处插入一个元素
    cout << "在第三个位置处插入元素 0.99" << endl;
    v.insert(v.begin()+2, 0.99);
    print(v);
     
    // 删除动态数组中的首元素
    cout << "删除动态数组中的首元素" << endl;
    v.erase(v.begin());
    print(v);
	
    return 0;
}

运行结果:

往动态数组中添加三个元素:
 0.12  0.34  0.56

此时动态数组为:[0.12, 0.34, 0.56]

修改v[2]为0.78

此时动态数组为:[0.12, 0.34, 0.78]

在第三个位置处插入元素 0.99

此时动态数组为:[0.12, 0.34, 0.99, 0.78]

删除动态数组中的首元素

此时动态数组为:[0.34, 0.99, 0.78]

list

我们可以借助 list 来实现一个双向链表,用它管理数据。

往双向链表中添加元素时,只需更改几个指针的指向即可,效率较高,其算法复杂度为 O(1)O(1),但是搜索算法的复杂度为 O(N)O(N),这种特点和 vector 刚好相反。

在使用双向链表之前,我们需要将 STL 中的 list 包含到程序中,即:

#include<list>

好了,现在我们来声明一个双向链表 L ,用于管理 char 类型的元素。

list<char> L;

如上所示,STL 提供的 list 是一个模板,我们需要在 <> 中指定栈容器要管理的数据类型。

list 既可以像 vector 一样通过 “[ ]” 运算符直接访问特定元素,也可以用迭代器逐个进行访问。

有了双向链表 L 后,我们可以调用其成员函数,如下所示:

  • push_front(x):将元素 x 添加到双向链表的开头
  • push_back(x):将元素 x 添加到双向链表的末尾
  • pop_front():删除双向链表的开头元素
  • pop_back():删除双向链表的末尾元素
  • size():返回双向链表的元素数
  • insert(p, x):在双向链表中位置为 p 处插入元素 x
  • erase(p):删除双向链表中位置为 p 的元素
  • clear():删除双向链表中的所有元素
  • begin():返回指向双向链表开头的迭代器
  • end():返回指向双向链表最后一个元素的后一个位置的迭代器

下面举例了双向链表的使用:

#include<iostream>
#include<list>
using namespace std;

void print(list<char> l) {
	cout << "\n此时双向链表为:" ;
	auto it = l.begin();
	while(1) {
		cout << "[" << *it << "]";
		it++;
		if(it != l.end()) cout << " <--> "; 
		else break;
	}
	cout << "\n\n";
}

int main() {
    // 声明一个双向链表 
    list<char> L;
    
    // 添加三个元素
    cout << "往双向链表中添加三个元素:\n a  b  c" << endl;
    L.push_back('b');
    L.push_front('a');
    L.push_back('c');
    print(L);
    
    // 删除双向链表的首元素 
    cout << "删除双向链表的首元素" << endl;
    L.pop_front();        // 或者:L.erase(L.begin());
    print(L);
    
    // 往双向链表的末尾处添加一个元素 d
    cout << "往双向链表的末尾处添加一个元素 d" << endl;
    L.push_back('d');        // 或者:L.insert(L.end(), 'd');
    print(L); 
	
    return 0;
}

运行结果:

往双向链表中添加三个元素:
 a  b  c

此时双向链表为:[a] <--> [b] <--> [c]

删除双向链表的首元素

此时双向链表为:[b] <--> [c]

往双向链表的末尾处添加一个元素 d

此时双向链表为:[b] <--> [c] <--> [d]