栈
栈是一个线性的数据结构,规定这个数据结构只允许在其中一端进行操作,并禁止直接访问除这一端以外的数据。
栈分为数组栈和链表栈,其区别是数组栈使用数组进行功能的模拟,实现较为快速和便利,而链表栈使用链表的思路去设计,实现较为麻烦,但是其稳定不易出错;在链表栈中又分为静态链表栈和动态链表栈,静态链表栈给定栈的空间大小,不允许超过存储超过给定数据大小的元素,而动态栈使用的是自动创建空间的方法进行创建,只要符合机器的硬件要求以及编译器的控制,其理论上是极大的。
说了那么多,我们以链表栈的动态链表栈为例子,进行栈的设计,在后文直接以栈一名字称呼动态链表栈,这也是各类语言标准模板中栈的实现方式。
首先是栈的结点设计,我们可以设计出两个结构体,一个结构体Node表示结点,其中包含有一个data域和next指针。
其中data表示数据,其可以是简单的类型(如int,double等等),也可以是复杂的结构体(struct类型);
next指针表示,下一个的指针,其指向下一个结点,通过next指针将各个结点链接。
目前的设计如同单链表,接下来,为这个进行限制性的设计,我们额外添加一个结构体,其包括了一个永远指向栈头的指针top和一个计数器count记录元素个数,(也可以设计成一个指针top和一个指针bottom分别指向栈头和栈尾)其主要功效就是设定允许操作元素的指针以及确定栈何时为空(count的方法是当count为0时为空,top和bottom方法就是两者指向同一个空间时为栈为空)
这里我采用的是top和count组合的方法。其代码可以表示为:
//栈的结点设计
//单个结点设计,数据和下一个指针
typedef struct node
{
int data;
struct node *next;
} Node;
//利用上面的结点创建栈,分为指向头结点的top指针和计数用的count
typedef struct stack
{
Node *top;
int count;
} Link_Stack;
//入栈 push
Link_Stack *Push_stack(Link_Stack *p, int elem)
{
if (p == NULL)
return NULL;
Node *temp;
temp=(Node*)malloc(sizeof(Node));
//temp = new Node;
temp->data = elem;
temp->next = p->top;
p->top = temp;
p->count++;
return p;
}
//出栈 pop
Link_Stack *Pop_stack(Link_Stack *p)
{
Node *temp;
temp = p->top;
if (p->top == NULL)
{
printf("错误:栈为空");
return p;
}
else
{
p->top = p->top->next;
free(temp);
//delete temp;
p->count--;
return p;
}
}
队列queue
顺序队列
队列是一个先进先出的数据结构。
队列(queue)是限定在表的一端进行插入,表的另一端进行删除的数据结构,如同栈的学习,请联系前文所学链表,试想一个单链表,我们只能对他的链表表尾进行插入,而只能对链表的表头进行结点的删除,其余一切的操作均不允许,这样强限制性的“链表“,就是我们所说的队列。
让我们重新理顺一下定义:队列是一个线性的数据结构,规定这个数据结构只允许在一端进行插入,另一端进行删除,禁止直接访问除这两端以外的一切数据。
如图:队列就像一个两端相通的水管,只允许一端插入,另一端取出,先放入管中的球先从管中拿出。
当如果队列不为空的时候,我们只需要将尾结点向后移动,通过不断移动next指针指向新的结点构成队列即可。
//入队操作
void push(queue *q,int data){
node *n =init_node();
n->data=data;
n->next=NULL; //采用尾插入法
//if(q->rear==NULL){ //使用此方法也可以
if(empty(q)){
q->front=n;
q->rear=n;
}else{
q->rear->next=n; //n成为当前尾结点的下一结点
q->rear=n; //让尾指针指向n
}
}
循环队列
对于顺序队列而言,其存在已经足够解决大多时候的设计问题了,但是其依旧存在一些缺陷和不足,因为我们的入队和出队操作均是直接在其后面进行结点的链接和删除,这就造成其使用空间不断向出队的那一边偏移,产生假溢出。
可能这个时候会产生一个疑问,我们学习的队列不是使用链表实现的动态队列么?没有空间的时候会开辟空间,这难道还会产生假溢出么?
是的的确,当进行动态创建队列的时候,也只不过是向后继续不断的申请内存空间,即时前面出队操作释放掉了前面的空间,但是指针依旧会向后进行移动,直到达到系统预留给程序的内存上界被强行终止,这对于极为频繁的队列操作和程序而言是致命的,这时候,就需要对我们的队列进行优化,使用更为优秀的结构——循环队列。
#include<stdio.h>
#include<stdlib.h>
#include<cstring>
#define maxsize 10 //表示循环队列的最大容量
//循环队列的结构设计
typedef struct cir_queue{
int data[maxsize];
int rear;
int front;
}cir_queue;
//初始化
cir_queue *init(){
cir_queue *q = (cir_queue*)malloc(sizeof(cir_queue));
if(q==NULL){
exit(0); //申请内存失败,退出程序
}
memset(q->data,0,sizeof(q->data));
q->front=0;
q->rear=0;
return q;
}
//入队操作push
void push(cir_queue *q,int data){
if((q->rear+1)%maxsize==q->front){
printf("溢出,无法入队\n");
return;
}else{
q->rear=(q->rear+1)%maxsize;
q->data[q->rear]=data;
}
}
//出队操作pop
void pop(cir_queue *q){
if(q->rear==q->front){
printf("队列为空,无法出队\n");
return;
}else{
q->data[q->front]=0;
q->front=(q->front+1)%maxsize;
}
}
//遍历队列
void print(cir_queue *q){
int i=q->front;
while(i!=q->rear){
i=(i+1)%maxsize;
printf("%d\t",q->data[i]);
}
printf("\n"); //记得换行
}
int main(){
cir_queue *q = init();
入队操作///
printf("入队操作\n");
for(int i=1;i<=6;i++){
push(q,i);
}
print(q);
出队操作///
printf("出队操作\n");
for(int i=1;i<=3;i++){
pop(q);
print(q);
}
return 0;
}
多返回值
- 全局变量,为什么不用它?
如,这样的方法,当我们需要通过函数对多个值进行返回和传递的时候,可以使用一种弄虚作假的方式,就是使用全局变量,不需要函数返回,只需要在关键时刻进行设置就可以了。
int x,y;
void getWay(int a,int b){
x=a,y=b;
}
int main(){
getWay(10,20);
cout<<x<<" "<<y<<endl;
return 0;
}
这是在自欺欺人,对,当我们需要多个值进行返回传递的时候,我们可以使用全局变量避免掉这个设计,但这并不能解决核心问题,使用全局变量当我们需要多次调用或者递归调用函数时则会出现诸如数据紊乱等的错误,而且一旦在工程中使用大量的全局变量,则会造成很多意外的后果,因此这里不过多讨论。
- 双返回值,pair
在我们学习pair一对数据的时候我们就有了解,我们可以通过pair作为数据类型进行多组数据的传递,这往往对于两个数据(较少的数据)而言,是最理想的情况。
pair<string,int> getClass(int id){
return make_pair("DOTCPP!",id);
}
int main(int argc,char **argv){
pair<string,int> a;
a=getClass(10);
cout<<a.first<<" "<<a.second<<endl;
return 0;
}
- 指针返回法
指针返回法(又名数组返回法)顾名思义,我们的数据类型使用的是一个指针类型的数组作为返回类型,其返回的内容在内存空间上是连贯的,这个方法也被用来进行常规的数组作为参数的返回。
#include<iostream>
using namespace std;
int *getWay(int n){
int *p=new int[3];
p[0]=n+10;
p[1]=n+20;
p[2]=n+30;
return p;
}
int main(){
int *res = getWay(10);
for(int i=0;i<3;i++){
cout<<res[i]<<' ';
}
delete []p;//防止内存泄漏
return 0;
}
4.结构体返回法
这个是对于超过两个元素的最推荐的写法了,对于指针返回法,其是相当于把多组的数据当成一个共同类型的参数作为处理,可一旦返回的元素同时有整形又有浮点型这样不同的元素,指针返回的方法往往就不太适用了,最理想的方式还是设计结构体,使用结构体返回法。
#include<iostream>
using namespace std;
struct ans
{
int a;
char b;
double c;
};
ans getWay(int n){
struct ans r_ans;
r_ans.a=n;
r_ans.b=n;
r_ans.c=n+0.11;
return r_ans;
}
int main(){
ans ans = getWay(97);
cout<<ans.a<<' '<<ans.b<<' '<<ans.c<<endl;
return 0;
}
//output:
//97 a 97.11
这样有点类似于在第二第三章所学的数据结构定义的内容,事实上的确是相通的,函数多返回值是C/C++设计上所缺失的,也由于时代原因当时的设计不需要考虑如今那么多,而如今的各种新型语言均支持了这个概念,直接使用逗号进行分割即可达到效果。
树
//创建树--插入数据
void insert(Tree* tree, int value){
//创建一个节点,让左右指针全部指向空,数据为value
Node* node=(Node*)malloc(sizeof(Node));
node->data = value;
node->left = NULL;
node->right = NULL;
//判断树是不是空树,如果是,直接让树根指向这一个结点即可
if (tree->root == NULL){
tree->root = node;
} else {//不是空树
Node* temp = tree->root;//从树根开始
while (temp != NULL){
if (value < temp->data){ //小于就进左儿子
if (temp->left == NULL){
temp->left = node;
return;
} else {//继续往下搜寻
temp = temp->left;
}
} else { //否则进右儿子
if (temp->right == NULL){
temp->right = node;
return;
}
else {//继续往下搜寻
temp = temp->right;
}
}
}
}
return;
}
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
复习了一遍,学了一次树快忘完了。。。
//树的先序遍历 Preorder traversal
void preorder(Node* node){
if (node != NULL)
{
printf("%d ",node->data);
inorder(node->left);
inorder(node->right);
}
}
//树的中序遍历 In-order traversal
void inorder(Node* node){
if (node != NULL)
{
inorder(node->left);
printf("%d ",node->data);
inorder(node->right);
}
}
//树的后序遍历 Post-order traversal
void postorder(Node* node){
if (node != NULL)
{
inorder(node->left);
inorder(node->right);
printf("%d ",node->data);
}
}
深度优先搜索
深度优先搜索算法(英语:Depth-First-Search,简称DFS)是一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n),DFS搜索的过程访问可以称之为DFS序。
而即时对于一颗深度为n的二树,在没有任何优化的情况下适用DFS去搜索访问数据,其算法的时间复杂度也高达O(2^n),在数据较大的情况下DFS是无法满足程序的时间要求,这就会涉及到一个思路——剪枝,即通过现有的数据判断接下来的数据无法再满足解,直接将当前结点以后的所有数据舍弃,遍历不再访问,通过精心设计的剪纸可以使得DFS搜索的效果得到很大提升。
bool check(参数)
{
if(满足条件)
return true ;
return false;
}
void dfs(int step)
{
判断边界
{
相应操作
}
尝试每一种可能
{
满足check条件
标记
继续下一步dfs(step+1)
恢复初始状态(回溯的时候要用到)
}
}