C/C++——数据结构与LetCode经典笔试

11 篇文章 2 订阅
10 篇文章 0 订阅

目录

一、数据结构基础

二、letscode

2.1、存在重复元素 III

2.2 划分为k个相等的子集

2.3 任务调度器

2.4 数组中的第K个最大元素

2.5 搜索二维矩阵 II

2.6 下一个排列


 

前言:理解数据结构的知识是有必要的,但是使用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;

循环链表:只需要将表中最后一个结点的指针指向头结点;

栈和队列:严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为 "一对一" 的数据既然栈和队列都属于线性表,根据线性表分为顺序表和链表的特点,栈也可分为顺序栈和链栈,队列也分为顺序队列和链队列

栈:栈只能从表的一端存取数据,另一端是封闭的;在栈中,无论是存数据还是取数据,都必须遵循"先进后出"的原则,即最先进栈的元素最后出栈。因此栈的定义为栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。

队列:队列的两端都"开口",要求数据只能从一端进,从另一端出,队列中数据的进出要遵循 "先进先出" 的原则;

串:数据结构中,字符串要单独用一种存储结构来存储,称为串存储结构,这里的串指的就是字符串。严格意义上讲,串存储结构也是一种线性存储结构,因为字符串中的字符之间也具有"一对一"的逻辑关系。只不过,与之前所学的线性存储结构不同,串结构只用于存储字符类型的数据。

字符串的三中存储方式:

  1. 定长顺序存储:实际上就是用普通数组(又称静态数组)存储。例如 C 语言使用普通数据存储字符串的代码为 char a[20] = "data.biancheng.net";
  2. 堆分配存储:用动态数组存储字符串;
  3. 块链存储:用链表存储字符串;

数组结构:一维数组,指的是存储不可再分数据元素的数组;二维数组,指的存储一维数组的一维数组;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);一棵树的深度(高度)是树中结点所在的最大的层次;

有序树和无序树:如果树中结点的子树从左到右看,谁在左边,谁在右边,是有规定的,这棵树称为有序树;反之称为无序树。

二叉树:是树的一种具体结构,包括满二叉树和完全二叉树。二叉树的存储结构有两种,分别为顺序存储和链式存储;

二叉树满足两个条件:

  1. 本身是有序树;
  2. 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;
  3. 二叉树中,第 i 层最多有 2i-1 个结点;
  4. 如果二叉树的深度为 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.csdn.net/qq_43496675/article/details/105762929

DFS解题思路:https://blog.csdn.net/yaoayao470/article/details/85064827

分治排序:https://www.cnblogs.com/chengxiao/p/6194356.html

https://www.cnblogs.com/fuxianfeng1988/p/3307016.html

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值