数据结构概览
处处逢归路,头头达故乡。
新学期要学习计算机领域中很重要的一门专业课程–《数据结构与算法》,每一个程序中都涉及数据结构和算法,同时,数据结构也是考研的必考专业课,面试过程中,也会考察数据结构的使用以及算法思维等等,由此可见这门课程的重要性。
在老师正式上课前,不妨先和我一起了解一下什么是数据结构,并回顾一下之前接触过的一些数据结构,同时,也概括性的了解以下这个学期要学什么东西,先提前有个整体的了解和规划。
我是十四运群众演员,要在西安工业大学封闭至9月16日开幕式结束,所以这半个月我无法开展见面会和迎新工作,更不能和你一起,希望等我回去之后 一切能回到正轨吧。
什么是数据结构?
数据结构是为实现对计算机数据有效使用的各种数据组织形式,服务于各类计算机操作。
不同的数据结构具有各自对应的适应场景,旨在降低各种算法计算的时间与空间复杂度,达到最佳的任务执行效率。
此时的你可能对于前面提到的时间复杂度与空间复杂度感到一头雾水,没关系,数据结构的第一节课老师就会教的,简而言之,时间/空间复杂度就是衡量算法效率的一个尺标。
时间复杂度主要体现的是算法(程序)的耗时问题,即语句的执行次数问题。
空间复杂度体现的就是算法对于计算机内存的需求问题。
常见的数据结构?
常见的数据结构总体可分为两类
【线性数据结构】和【非线性数据结构】.
具体为
数组、链表、栈、对联、树、图、散列表、堆
下面我们来逐一了解一下
数组
在C++中,数组是将相同类型的元素存储于连续内存空间的数据结构,其长度一般不可变。
在Python中,数组可以存储不同类型的数据,这是Python语言的特性。
在C++中,构建数组需要在初始化时给定长度,并对数组每个索引元素赋值。
注意:索引从0开始!
// 初始化一个长度为 5 的数组 array
int array[5];
// 元素赋值
array[0] = 2;
array[1] = 3;
array[2] = 1;
array[3] = 0;
array[4] = 2;
也可以使用直接赋值的初始化方式。
int array[]={2,3,0,1,2};
可变数组是我们经常使用的数据结构,它基于数组和扩容机制实现,更加的灵活,常用的操作有访问元素、添加元素、删除元素。
在C++中,我们所说的可变数组一般就是vector向量容器,vector的操作回头我会单独总结。
// 初始化可变数组
vector<int> array;
// 向尾部添加元素
array.push_back(2);
array.push_back(3);
array.push_back(1);
array.push_back(0);
array.push_back(2);
vect0r.push_back() 方法用于向vector中压入元素。
链表
链表我们以前也接触过,大一上的时候不少同学的课设就使用了链表作为数据存储方式。
链表以节点为单位,每个元素都是一个独立对象,在内存空间的存储是非连续的。
链表的节点对象具有两个成员变量:
值[val],后继节点[next]
struct ListNode
{
int val; //节点值
ListNode *next; //后继节点引用
ListNode(int x):val(x),next(NULL) {}
};
不知道上述代码你能不能看得懂。
结构体其实就是类的原始版(低级版),它除了能够携带数据外,也可以携带函数。
可以把它当成一个类去看。
那么
ListNode(int x):val(x),next(NULL) {}
这一行怎么理解呢?
相当于构造函数 并采用参数列表进行初始化 为结构体中的节点值成员val赋初值x,后继节点默认赋为空。
如下图所示,建立此链表需要实例化每个节点,并构建哥节点的引用指向。
// 实例化节点
ListNode *n1 = new ListNode(4); // 节点 head
ListNode *n2 = new ListNode(5);
ListNode *n3 = new ListNode(1);
// 构建引用指向
n1->next = n2;
n2->next = n3
栈
栈是什么呢?
单词是哪一个呢? STACK
特性是什么呢? 先入后出
我们总是能看见栈这种数据结构。
栈(Stack)是一种具有先入后出特点的抽象数据结构,底层使用数组或链表实现。
C++中预封装好了栈类 stack。
使用前包含头文件即可
#include<stack>
stack<int> stk; //创建一个栈名为stk 存储int型数据
//栈的特性:先入后出!
stk.push(1); //元素1入栈
stk.push(2); //元素2入栈
stk.pop(); //出栈->元素2
stk.pop(); //出栈->元素1
栈类的操作会在之后文章中讲解。
常用操作有入栈(push),出栈(pop)等等。
队列
队列(queue)是一种具有先入先出特点的抽象数据结构,底层由链表实现。
同样的,C++中预封装好了队列类queue,包含头文件即可。
但到后面学习的时候,肯定要自己手撕实现一遍。
#include<queue>
queue<int> que; //创建一个存储int型数据的队列 名为que
que.offer(1); //元素1入队
que.offer(2); //元素2入队
que.poll(); //出队->元素1
que.poll(); //出队->元素2
树
树是一种非线性数据结构,根据子节点数量可以分为二叉树和多叉树,最顶层的节点称为根节点(root)。
以二叉树为例,每个节点包含三个成员变量:值(val)、左子节点(left)、右子节点(right)
struct TreeNode {
int val; // 节点值
TreeNode *left; // 左子节点
TreeNode *right; // 右子节点
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
建立此二叉树需要实例化每个节点,并构建各节点的引用指向。
// 初始化节点
TreeNode *n1 = new TreeNode(3); // 根节点 root
TreeNode *n2 = new TreeNode(4);
TreeNode *n3 = new TreeNode(5);
TreeNode *n4 = new TreeNode(1);
TreeNode *n5 = new TreeNode(2);
// 构建引用指向
n1->left = n2;
n1->right = n3;
n2->left = n4;
n2->right = n5
图
图(map)是一种非线性数据结构。
由节点(顶点)(vertex)和边(edge)组成,每条边连接一对顶点。
根据边的方向有无,图可分为有向图和无向图
以无向图为例介绍。
图中无向图的顶点和边集合分别为:
顶点集合: vertices = {1, 2, 3, 4, 5}
边集合: edges = {(1, 2), (1, 3), (1, 4), (1, 5), (2, 4), (3, 5), (4, 5)}
表示图的方法通常有两种:
1、邻接矩阵:使用数组vertices存储顶点,邻接矩阵edges存储边;
edges [i] [j] 代表节点 i+1 和节点 j+1 之间是否有边。
int vertices[5] = {1, 2, 3, 4, 5};
int edges[5][5] = {{0, 1, 1, 1, 1},
{1, 0, 0, 1, 0},
{1, 0, 0, 0, 1},
{1, 1, 0, 0, 1},
{1, 0, 1, 1, 0}};
2、邻接表:使用数组vertices存储顶点,邻接表edges存储边。
edges为一个二维容器,其第一维i代表顶点索引,第二维edges[i]存储此顶点对应的边集合;
例如 edges[0]=[1,2,3,4]edges[0] = [1, 2, 3, 4]edges[0]=[1,2,3,4] 代表 vertices[0]vertices[0]vertices[0] 的边集合为 [1,2,3,4][1, 2, 3, 4][1,2,3,4] 。
这个讲的不是很清楚,我想想怎么能更直观的讲,看不懂正常。
int vertices[5] = {1, 2, 3, 4, 5};
vector<vector<int>> edges;
vector<int> edge_1 = {1, 2, 3, 4};
vector<int> edge_2 = {0, 3};
vector<int> edge_3 = {0, 4};
vector<int> edge_4 = {0, 1, 4};
vector<int> edge_5 = {0, 2, 3};
edges.push_back(edge_1);
edges.push_back(edge_2);
edges.push_back(edge_3);
edges.push_back(edge_4);
edges.push_back(edge_5);
邻接矩阵 VS 邻接表 :
邻接矩阵的大小只与节点数量有关,即 N2N^2N2 ,其中 NNN 为节点数量。因此,当边数量明显少于节点数量时,使用邻接矩阵存储图会造成较大的内存浪费。
因此,邻接表 适合存储稀疏图(顶点较多、边较少); 邻接矩阵 适合存储稠密图(顶点较少、边较多)。
散列表
散列表–unordered map
散列表是一种非线性数据结构。
通过利用Hash函数将指定的键(key)映射至对应的值(value),以实现高效的元素查找。(有点点像字典)。
一个简单场景:Peter、Jack、Parker的学号分别为 10001, 10002, 10003 。
现需求从「姓名」查找「学号」。
那么我们就可以建立姓名为key,学号为value的散列表来实现此需求。
// 初始化散列表
unordered_map<string, int> dic;
// 添加 key -> value 键值对
dic["Peter"] = 10001;
dic["Jack"] = 10002;
dic["Parker"] = 10003;
// 从姓名查找学号
dic.find("Peter")->second; // -> 10001
dic.find("Jack")->second; // -> 10002
dic.find("Parker")->second; // -> 10003
设计一个简易Hash函数
需求:根据学好查找姓名。
将三人的姓名存储至以下数组中,则各姓名在数组中的索引分别为0,1,2.
string names[] = { "小力", "小特", "小扣" };
此时,我们构造一个简单的Hash函数(%取余)
int hash(int id)
{
int index = (id - 1) % 10000;
return index;
}
那么,我们就构建了以学号为ket、姓名对应的数组索引为value的散列表。利用此Hash函数,就可以在O(1)时间复杂度下通过学号直接查找到对应姓名。
即
names[hash(10001)] // 小力
names[hash(10002)] // 小特
names[hash(10003)] // 小扣
堆
堆 Heap
堆是一种基于完全二叉树的数据结构,可以使用数组实现。
以堆为原理的排序算法称为堆排序,基于堆实现的数据结构为优先队列。
堆分为大顶堆和小顶堆
大(小)顶堆:任意节点的值不大于(小于)其父节点的值。
完全二叉树:
设二叉树深度为k,若二叉树除第k层外的其它各层(第1至k-1层)的节点达到最大个数,且处于第k层的节点都连续集中在最左边,则称此二叉树为完全二叉树。
如下图所示,为包含 1, 4, 2, 6, 8
元素的小顶堆。将堆(完全二叉树)中的结点按层编号,即可映射到右边的数组存储形式。
可以通过使用优先队列的压入(push())和弹出(pop())操作,完成堆排序。
// 初始化小顶堆
priority_queue<int, vector<int>, greater<int>> heap;
// 元素入堆
heap.push(1);
heap.push(4);
heap.push(2);
heap.push(6);
heap.push(8);
// 元素出堆(从小到大)
heap.pop(); // -> 1
heap.pop(); // -> 2
heap.pop(); // -> 4
heap.pop(); // -> 6
heap.pop(); // -> 8
,即可映射到右边的数组存储形式。
[外链图片转存中…(img-ultBgXlZ-1630397117871)]
可以通过使用优先队列的压入(push())和弹出(pop())操作,完成堆排序。
// 初始化小顶堆
priority_queue<int, vector<int>, greater<int>> heap;
// 元素入堆
heap.push(1);
heap.push(4);
heap.push(2);
heap.push(6);
heap.push(8);
// 元素出堆(从小到大)
heap.pop(); // -> 1
heap.pop(); // -> 2
heap.pop(); // -> 4
heap.pop(); // -> 6
heap.pop(); // -> 8