目录
前言:理解数据结构的知识是有必要的,但是使用C来编写这些数据结构的工作(轮子)已经被C++做好了,所以在什么情况下使用什么样的轮子使我们考虑的。
一、数据结构基础
- 数据结构研究数据的存储方式,使数据之间有一定逻辑关系,方便后期对数据的再利用和管理;即如何存储具有复杂关系的数据更有助于后期对数据的再利用。
- 数据结构存储结构:
线性表:将具有“一对一”关系的数据“线性”地存储到物理空间中,这种存储结构就称为线性存储结构;线性表结构存储的数据往往是可以依次排列的,线性表并不是一种具体的存储结构,它包含顺序存储结构和链式存储结构,即顺序表和链表。
顺序表:数组
typedef struct Table{
int * head;//数组指针
int length;//元素个数
int size;//内存大小
}table;
链表:链表给各数据块增设一个指针,每个数据块的指针都指向下一个数据块(最后一个数据块的指针指向 NULL),链接起来的内存空间称为链表
typedef struct Link{
char elem; //代表数据域
struct Link * next; //代表指针域,指向直接后继元素
}link; //link为节点名,每个节点都是一个 link 结构体
栈和队列隶属于线性表,是特殊的线性表(两种数据存储类型),因为它们对线性表中元素的进出做了明确的要求,栈(后入先出,栈顶入栈,栈顶出栈),队列(先入先出,队尾入队,队首出队);
树结构:适合存储具有“一对多”(节点)关系的数据
图结构:适合存储具有“多对多”关系的数据。
补充:
静态链表:兼顾了顺序表和链表的优点于一身,可以看做是顺序表和链表的升级版;数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间"一对一"的逻辑关系通过一个整形变量(称为"游标",和指针功能类似)维持(和链表类似)
typedef struct {
int data;//数据域
int cur;//游标
}component;
上述静态链表不完整,还需要备用链表;静态链表中,除了数据本身通过游标组成的链表外,还需要有一条连接各个空闲位置的链表,称为备用链表;备用链表的作用是回收数组中未使用或之前使用过(目前未使用)的存储空间,留待后期使用。也就是说,静态链表使用数组申请的物理空间中,存有两个链表,一条连接数据,另一条连接数组中未使用的空间;
单向链表:表中各节点中都只包含一个指针(游标),且都统一指向直接后继节点,通常称这类链表为单向链表
双向链表:同单链表相比,双链表仅是各节点多了一个用于指向直接前驱的指针域。因此,我们可以在单链表的基础轻松实现对双链表的创建
typedef struct line{
struct line * prior; //指向直接前趋
int data;
struct line * next; //指向直接后继
}line;
循环链表:只需要将表中最后一个结点的指针指向头结点;
栈和队列:严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为 "一对一" 的数据既然栈和队列都属于线性表,根据线性表分为顺序表和链表的特点,栈也可分为顺序栈和链栈,队列也分为顺序队列和链队列;
栈:栈只能从表的一端存取数据,另一端是封闭的;在栈中,无论是存数据还是取数据,都必须遵循"先进后出"的原则,即最先进栈的元素最后出栈。因此栈的定义为栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。
队列:队列的两端都"开口",要求数据只能从一端进,从另一端出,队列中数据的进出要遵循 "先进先出" 的原则;
串:数据结构中,字符串要单独用一种存储结构来存储,称为串存储结构,这里的串指的就是字符串。严格意义上讲,串存储结构也是一种线性存储结构,因为字符串中的字符之间也具有"一对一"的逻辑关系。只不过,与之前所学的线性存储结构不同,串结构只用于存储字符类型的数据。
字符串的三中存储方式:
- 定长顺序存储:实际上就是用普通数组(又称静态数组)存储。例如 C 语言使用普通数据存储字符串的代码为 char a[20] = "data.biancheng.net";
- 堆分配存储:用动态数组存储字符串;
- 块链存储:用链表存储字符串;
数组结构:一维数组,指的是存储不可再分数据元素的数组;二维数组,指的存储一维数组的一维数组;n 维数组,指的是存储 n-1 维数组的一维数组
//以行序为主方式:按照列号从小到大顺序,依次存储每一行元素,在二维数组 anm 中查找 aij 存放位置公式为
LOC(i,j) = LOC(0,0) + (i*m + j) * L;
//以列存储的方式:按照行号从小到大的顺序,依次存储每一列的元素,在 anm 中查找 aij 的方式为
LOC(i,j) = LOC(0,0) + (i*n + j) * L;
广义表:又称列表,也是一种线性存储结构。同数组类似,广义表中既可以存储不可再分的元素,也可以存储广义表,例如存储 {1,{1,2,3}} 这样的数据
树结构:树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合;术语:父节点,子节点,兄弟节点,根节点,子树;对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree);一棵树的深度(高度)是树中结点所在的最大的层次;
有序树和无序树:如果树中结点的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树;反之称为无序树。
二叉树:是树的一种具体结构,包括满二叉树和完全二叉树。二叉树的存储结构有两种,分别为顺序存储和链式存储;
二叉树满足两个条件:
- 本身是有序树;
- 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;
- 二叉树中,第 i 层最多有 2i-1 个结点;
- 如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个结点
满二叉树:如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树
完全二叉树:如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树
顺序存储:只适用于完全二叉树
链表存储:
typedef struct BiTNode{
TElemType data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子指针
struct BiTNode *parent;
}BiTNode,*BiTree;
图结构:数据之间的关系有 3 种,分别是 "一对一"、"一对多" 和 "多对多",前两种关系的数据可分别用线性表和树结构存储,第三张使用图结构方式存储数据。图中的点称为顶点,图结构有两种类型:有向图和无向图。无向图中描述两顶点(V1 和 V2)之间的关系可以用 (V1,V2) 来表示,而有向图中描述从 V1 到 V2 的"单向"关系用 <V1,V2> 来表示。
根据不同的特征,图又可分为完全图,连通图、稀疏图和稠密图:
完全图:若图中各个顶点都与除自身外的其他顶点有关系,这样的无向图称为完全图;同时,满足此条件的有向图则称为有向完全图。
连通图:图中从一个顶点到达另一顶点存在至少一条路径,则称这两个顶点是连通着的。无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图。
稀疏图和稠密图:这两种图是相对存在的,即如果图中具有很少的边(或弧),此图就称为"稀疏图";反之,则称此图为"稠密图"(稀疏和稠密的判断条件是:e<nlogn,其中 e 表示图中边(或弧)的数量,n 表示图中顶点的数量。如果式子成立,则为稀疏图;反之为稠密图)。
生成树:对连通图进行遍历,过程中所经过的边和顶点的组合可看做是一棵普通树,称为生成树(生成树是对应连通图来说,而生成森林是对应非连通图来说的)
图的存储结构包括顺序存储结构、邻接表存储结构、十字链表存储结构和邻接多重表存储结构;对图的顶点进行遍历通常包括两种方法深度优先搜索(DFS:deep first search)和广度优先搜索(BFS:Breadth first search)。
深度优先搜索DFS:是从图中的一个顶点出发,每次遍历当前访问顶点的邻接点,一直到访问的顶点没有未被访问过的邻接点为止。然后采用依次回退的方式,查看来的路上每一个顶点是否有其它未被访问的邻接点。访问完成后,判断图中的顶点是否已经全部遍历完成,如果没有完全遍历,以未访问的顶点为起始点,重复上述过程(回溯法)。
广度优先搜索BFS: 广度优先搜索类似于树的层次遍历。从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点。按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
总结:深度优先搜索算法的实现运用的主要是回溯法,类似于树的先序遍历算法。广度优先搜索算法借助队列的先进先出的特点,类似于树的层次遍历。
(什么是回溯法:又称为“试探法”。解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再进行新的尝试的方法就是回溯算法。)
//DFS 解题框架思路:确定解空间,从开始节点深度优先搜索整个解空间,如果扩展节点不能继续纵向移动
//返回移动到最近的一个活结点
void dfs(int deep, State curState)
{
if (deep > Max) //深度达到极限
{
if (curState == target) //找到目标
{
//...
}
}
else
{
for (i = 1; i <= totalExpandMethod; i++)
{
dfs(deep + 1,expandMethod(curState, i));
}
}
}
//递归回溯
void backtrack(int t){//当前结点在解空间的深度
if(t>n)
output(x);
else
for(int i=f(n,t);i<=g(n,t)i++)
{//所有儿子
x[t]=h(i);
if( constraint(t)&&bound(t) )//约束条件&&限界条件
backrtrack(t+1);
}
}
查找表:用于查找操作的数据结构;一般操作包括在查找表中查找某个具体的数据元素;在查找表中插入数据元素;从查找表中删除数据元素。
静态查找表:在查找表中只做查找操作,而不改动表中数据元素;
动态查找表:在查找表中做查找操作的同时进行插入数据或者删除数据的操作
二叉查找树:动态查找表中做查找操作时,若查找成功可以对其进行删除;如果查找失败,即表中无该关键字,可以将该关键字插入到表中。二叉查找树使用树结构表示动态查找表的实现方法,特点:
- 二叉排序树中,如果其根结点有左子树,那么左子树上所有结点的值都小于根结点的值
- 二叉排序树中,如果其根结点有右子树,那么右子树上所有结点的值都大小根结点的值
- 二叉排序树的左右子树也要求都是二叉排序树
平衡二叉树:动态查表的第二种实现方式;在二叉树的基础上,若树中每棵子树都满足其左子树和右子树的深度差都不超过 1,则这棵二叉树就是平衡二叉树;特点:
- 每棵子树中的左子树和右子树的深度差不能超过 1
- 二叉树中每棵子树都要求是平衡二叉树
哈希表:通过关键字直接找到数据的存储位置,不需要进行任何的比较,其查找的效率相较于前面所介绍的查找算法是更高的;哈希表的构造方法包括:直接定址法、数字分析法、平方取中法、折叠法、除留余数法和随机数法。
二、letscode
2.1、存在重复元素 III
二叉树:
二叉树遍历:树结构分为广度优先遍历和深度优先遍历。广度遍历是一层一层的遍历树中的元素,这种遍历方式需要借助队列的方式,左右子树分别入队列,利用队列先进先出的特性,按层对树进行遍历。深度遍历有又分为前序遍历、中序遍历、后序遍历,针对这三种遍历方式,又有利用递归的形式进行遍历,利用堆栈结构进行遍历
字典树:又称前缀树,字典树常用于搜索提示,如当输入一个网址,可以自动搜索出可能的选择。字典树并不是二叉树,一个父节点可能会有多个子节点,故而在定义树的node类时,记录子节点的数据结构是dict。
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
set<long long> record;
for(int i = 0; i < nums.size(); i++){
if(record.lower_bound((long long)nums[i] - (long long)t) != record.end()
&& (long long)*record.lower_bound((long long)nums[i] - (long long)t) <= (long long)nums[i] + (long long)t)//差值条件
return true;
record.insert(nums[i]);
if(record.size() > k) //范围内查找
record.erase(nums[i - k]);//删除第i个元素
}
return false;
}
};
set lower_bound(x)返回的是第一个大于等于x的迭代器;
lower_bound(*first,*last,val):在first和last中的前闭后开区间进行二分查找,返回大于或等于val的第一个元素位置。如果所有元素都小于val,则返回last的位置。
对比一下219存在重复元素 II的过程
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_set<int> record; //用于记录已经遍历过的数字
for(int i = 0; i < nums.size(); i++){
if(record.find(nums[i]) != record.end())
return true;
record.insert(nums[i]);
if(record.size() > k)
record.erase(nums[i - k]);
}
return false;
}
};
2.2 划分为k个相等的子集
分析:
- k个相等的子集也就是说这k个子集的和为sum,且每个子集的和为sum/k
- 在回溯中有两种情况:第一种是成功找到一个子集可以继续寻找下一个子集;第二种是在寻找子集中剩余大小元素(能否找到);
class Solution {
public:
int perSum = 0;//每个子集的和
bool canPartitionKSubsets(vector<int>& nums, int k)
{
unordered_map<int, int> hashNums;//统计n次数
int sum = 0, maxVal = 0;
for (auto n : nums)
{
hashNums[n]++;
sum += n;
maxVal = max(maxVal, n);
}
perSum = sum / k;
if ((sum%k != 0)||(maxVal>perSum))
return false;
return dfs(hashNums, k, 0);
}
//搜索第k个 persum集合
//target保存子集中剩余元素
bool dfs(unordered_map<int, int>& hashNum, int kth, int target)
{
if (target == 0)
{
if (kth == 0)
{
return true;
}
else
{
//寻找下一个子集
return dfs(hashNum, kth - 1, perSum);
}
}
else
{ //搜索target
for (int num = target; num>0; num--)
{
//存在并且还有使用次数
if (hashNum.count(num) && hashNum[num] > 0)
{
hashNum[num] -= 1;
if (dfs(hashNum, kth, target - num))
{
return true;
}
//恢复次数
hashNum[num] += 1;
}
}
return false;//没找到直接返回
}
}
};
深度优先搜索DFS/递归/回溯思路:
采用搜索算法解决问题时,需要构造一个表明状态特征和不同状态之间关系的数据结构,这种数据结构称为结点。不同问题需要用不同的数据结构描述。
2.3 任务调度器
贪心的思想:先把出现最多的任务分配了(即每隔n个单位时间分配一个任务),然后再把其它任务填上
结果的计算公式为:(x - 1) * (n + 1) + num
其中,x表示出现次数最多的任务的次数,n表示输入参数中的时间间隔,num表示出现次数为 x 的任务总数;
class Solution {
public:
static bool greaterSort(pair<char,int> pf,pair<char,int> pb)
{
return pf.second>pb.second;
}
int leastInterval(vector<char>& tasks, int n) {
map<char, int> mp;
//先将tasks中任务计数在map中
for( auto val:tasks)
mp[val]+=1;
//将map中的元素转到vector中,之后再进行sort排序,找到出现次数最多的任务
vector<pair<char, int> > count(mp.begin(),mp.end());
sort(count.begin(), count.end(), greaterSort);//升序
int num = 0; //用于记录出现次数最多的任务有多少个
for(int i = 0; i < count.size(); i++){
if(count[i].second == count[0].second)
num++;
}
//出现次数最多的任务为count[0].first, 其出现次数是count[0].second
int res = (count[0].second - 1) * (n + 1) + num;
return res<tasks.size()?tasks.size():res;
}
};
贪心算法框架:
从问题的某一初始解出发;
while (能朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;
2.4 数组中的第K个最大元素
问题:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
要求:使用分治法解决。
什么是分治?分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。
(https://www.cnblogs.com/chengxiao/p/6194356.html)
分治模式在每一层递归上都有三个步骤:
(1)分解(divide):将原问题分解成一系列子问题;
(2)解决(conquer):递归的解各个子问题。若子问题足够小,则直接求解;
(3)合并(combine):将子问题的结果合并成原问题的解;
2.5 搜索二维矩阵 II
思路:将右上角元素作为第一个比较对象,它是该列最小元素,是该行最大元素,采用淘汰思想即从上往下如果比target大,那么它该列下面的元素都不用找了,col--
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int row,col,m,n;
row = matrix.size();
col = matrix[0].size();
m=0;
n=col-1;
while(m<row && n>=0)
{
if(target == matrix[m][n])
return true;
else if(target < matrix[m][n])
n--;
else
m++;
}
return false;
}
};
2.6 下一个排列
void nextPermutation(vector<int>& nums)
{
int l = 0;
int r = nums.size() - 1;
//step1: 找l,最后一组升序数字的第一个位置
for (int i = 0; i<nums.size() - 1; i++)
{
if (nums[i]<nums[i + 1])
{
l = i;
}
}
//step2: 找r
for (int i = l + 1; i<nums.size(); i++)
{
if (nums[i]>nums[l])
{
r = i;
}
}
//step3: swap
int temp = 0;
temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
sort(nums.begin()+l+1,nums.end());
}
(二叉树220,递归迭代698,贪心算法621,分治法排序215/240,)
参考:
数据结构:http://c.biancheng.net/view/3328.html
回溯法:https://blog.youkuaiyun.com/qq_43496675/article/details/105762929
DFS解题思路:https://blog.youkuaiyun.com/yaoayao470/article/details/85064827
分治排序:https://www.cnblogs.com/chengxiao/p/6194356.html
https://www.cnblogs.com/fuxianfeng1988/p/3307016.html