前言
我学习数据结构时候就发现,数据结构本身是一种很抽象的东西,如果仅依靠看文字那真的是越看越糊涂,所以我觉得学习这玩意儿的时候一定要发挥自己的想象力想办法把这个东西具体化,这样才方便自身的理解以便日后称霸群雄。
数据结构其实就是用来储存数据一些容器结构而已,其功能就是记录数据。所以我认为学习数据结构就是要从中去找到一种更加方便更加简洁的方法来记录数据。
一、线性表
顾名思义就是一种一维的表结构,其实可以类比很早以前的结绳记事法,线性表结构就像一根绳子一样,而绳子上可能每间隔一段距离就有一张便签纸,便签纸里面的内容就是数据(data),每一个存储便签纸的节点都有一个编号,这就是索引(index)
顺序表
顺序表是用一组地址连续的存储空间依次存储线性表的数据元素。即线性表内的数据元素在计算机内的物理地址是相邻的。故其存储结构简单、空间利用率搞、存取数据速度也快。但对于数据的动态变化而言效率就没那么高了。例如:插入一个数据元素,那么这个插入的位置之后的所有数据都要向后移动一个位置,所以效率比较低。
存储结构表示
1.静态存储方法表示
顺序表的大小在声明时就已经确定好了,所以一旦数据空间占满了,再加入新的元素时就会溢出。
#define MAXSIZE 256
typedef struct
{
ElemType data[MAXSIZE]; //静态下的连续存储空间
int length; //记录存储数据元素个数
}SeqFixedList;
2.动态存储方法表示
顺序表的大小不固定,结构体中增加了size变量来记录当前存储空间的大小。需要通过动态存储分配语句malloc或new进行空间申请,所以可以扩充存储空间。
#define LISTINITSIZE 256 //初次分配空间大小
#define LISEINCREMENT 128 //空间分配增量大小
typedef struct SeqList
{
ElemType *pData; //顺序表的地址指针
int length; //记录存储数据元素个数
int size; //记录当前已经分配了的存储空间大小
}SeqList;
一些基本操作方法
1.初始化操作
Status InitList ( SeqList &L ) //初始化顺序表
{
K.pData = (ElemType *)malloc( LISTINITSIZE*sizeof(ElemType) );//申请存储空间
if ( L.pData= = NULL ) exit(OVERFLOW); //存储空间申请失败
L.size = LISTINITSIZE; //记录当前已经分配了的存储空间大小
L.length = 0; //存储数据元素个数设置为0
return OK;
}
2.销毁表
Status DestroyList ( SeqList &L ) //销毁顺序表
{
if( L.pData != NULL ) //如果指针pData所指地址存在不为空
{
free(L.pData); //释放指针所指位置的内存
L.pData = NULL; //将顺序表指针置空
}
L.size = 0; //当前已分配的存储空间大小记录值重置为0
L.length = 0; //存储数据元素个数为0
return OK;
}
3.清空表内元素
Status ClearList ( SeqList &L ) //清空顺序表
{
L.length = 0; //存储数据元素个数为0
return OK;
}
4.获取表内元素
Status GerElem( SeqList L, int i, ElemType &e )
{
if ( i < 1 || i > L.length ) //检查参数
return PARA_ERROR;
e = L.pData[i-1]; //获取数据元素
return OK;
}
5.表内元素查找
Status LocateElem ( SeqList L, ElemType &e ) //查找元素e所在的位置
{
for ( i=0; i<L.length; i++ ) //遍历整个顺序表
{
if ( L.pData[i] == e ) //筛选查找信息
return i+1; //查找成功,返回元素e的位置:第i+1个元素
}
return 0; //没有查找到返回 0
}
6.删除元素
Status DeleteList ( SeqList &L, int i, ElemType &e ) //将顺序表第i个元素删除,并返回此元素
{
if ( i < 1 || i > L.length ) //检查参数
return PARA_ERROR;
e = L.pData[i-1]; //将第i个元素存储在数组下标(索引)为 i-1 的位置上
for ( j = i; j <= L.length-1; j++ )
//从第i个(即第i-1后面的位置)到最后一个元素依次向前移动一个位置
{
L.pData[j-1] = L.pData[j];
}
L.length -= 1; //删除后顺序表长度-1
return 0; //查找失败,返回0
}
7.插入元素
Status InsertList ( SeqList &L, int i, ElemType &e ) //在顺序表第i个位置上插入元素e
{
if ( i < 1 || i > L.length+1 ) //检查参数
return PARA_ERROR;
if ( L.length >= L.size ) //当前存储空间已满,需要增加存储空间
{
newbase = ( ElemType* )realloc( L.elem, (L.size+LISTINCREMENT)*sizeof(ElemType) );
if ( newbase == NULL ) exit(OVERFLOW); //内存申请失败
L.pData = newbase;
L.size += LISTINCREMENT;
}
for ( j = L.length-1; j >= i-1; j-- )
//从第i个(即第i-1后面的位置)到最后一个元素依次向后移动一个位置
{
L.pData[j+1] = L.pData[j];
}
L.pData[i-1] = e; //在数组下标(索引)为 i-1 的位置上插入元素e
L.length += 1; //插入后顺序表长度+1
return OK; //成功
}
链表
物理位置上不要求相邻,通过指针来表示前驱和后继的关系,即由指针相连。多应用在类似插入和删除频繁存储空间需求不确定的情况下。当然缺点就是不能像顺序表那样随机存取。
单链表
存储结构
typedef struct LNode //结点结构
{
ElemType data; //数据域
struct LNode *next; //指针域
} LNode, *LinkList;
typedef struct SListInfo //表结构
{
LinkList head; //头结点指针
LinkList tail; //尾结点指针
LNode *pCurNode; //当前结点指针位置
int length; //单链表长度或元素个数
} SListInfo;
一些基本操作
双向链表
存储结构
typedef struct DuLNode //结点结构
{
ElemType data; //数据域
struct LNode *prev; //指向前驱结点的指针 前驱指针
struct LNode *next; //指向后继结点的指针 后继指针
} DuLNode, *DuLinkList;
typedef struct DuListInfo //表结构
{
DuLinkList head; //头结点指针
DuLinkList tail; //尾结点指针
DuLNode *pCurNode; //当前结点指针位置
int length; //双向链表长度或元素个数
} DuListInfo;
插入新结点
Status InsertElemBeforeCurNode( DuListInfo &L, ElemType e )
{
//1. 申请新的结点s
s = (DuLNode *)malloc(sizeof(DuLNode));
if ( s == NULL ) exit(OVERFLOW);//内存申请失败
s.data = e;
//2. 将结点s链接到pCurNode结点之前
s->prev = L.pCurNode->prev;
L.pCurNode->prev->next = s;
s->next = L.pCurNode;
L.pCurNode->prev = s;
//3. 双向链表长度+1
L.length += 1;
return OK;
}
删除结点
Status InsertElemBeforeCurNode( DuListInfo &L, ElemType e )
{
//1. 将待删除的结点的数据元素赋值给e
e = L.pCurNode->prev.data;
//2. 删除当前结点的前一个结点
p = L.pCurNode->prev;
p->prev->next = L.pCurNode;
L.pCurNode->prev = p->prev;
free(p);
//3. 双向链表长度-1
L.length -= 1;
return OK;
}
二、栈
其本质还是一种线性表。只不过它所定义的规则是只能在表的一端进行插入和删除操作。由此,栈又称为LIFO(Last In First Out 后进先出)的线性表。允许插入和删除操作的一端称为栈顶,其中插入操作又被称为压栈(入栈),删除操作又被称为弹栈(出栈);另一端则称为栈底。
空栈 满栈
顺序栈
可以用一块连续的存储单元作为其存储结构。存储空间既可以是静态分配的固定大小的一维数组,也可以是动态分配的一块连续的存储空间。
动态存储结构
#define STACKINITSIZE 256 //初次分配空间大小
#define STACKINCREMENT 128 //空间分配增量大小
typedef struct SeqStack
{
ElemType *pBase; //栈底指针
ElemType *pPop; //栈顶指针
int stacksize; //记录当前已经分配了的存储空间大小
}SeqList;
压栈
Status Push ( SeqStack &S, ElemType e )
{
if( S.pTop - S.pBase >= S.stacksize ) //当前存储空间已满,需增加存储空间
{
S.pBase = ( ElemType* ) realloc ( S.pBase, (S.stacksize + STACKINCREMENT)*sizeof(ElemType) );
if ( S.pBase == NULL ) exit(OVERFLOW); //内存申请失败
S.pTop = S.pBase + S.stacksize; //重新计算栈顶指针位置
S.stacksize += STACKINCREMENT;
}
*S.pTop = e; //插入元素e作为新的栈顶元素
S.pTop++; //栈顶指针指向栈顶下一个位置
return OK;
}
弹栈
Status Pop ( SeqStack &S, ElemType &e )
{
if ( S.pTop == S.pBase ) return ERROR; //空栈
e = *(S.pTop-1); //将栈顶元素值赋给e
S.pTop--; //栈顶指针指向信道栈顶位置
return OK;
}
链式栈
为方便压栈(入栈)和弹栈(出栈)常常将单链表的表头设置为栈顶,表尾设置为栈底。
存储结构
typedef struct LNode //结点结构
{
ElemType data; //数据域
struct LNode *next; //指针域
} LNode, *LinkList;
压栈
Status Push ( SeqStack &S, ElemType &e )
{
//1.申请新的结点s
s = (LNode *) maclloc (sizeof(LNode) );
if ( s == NULL ) exit(OVERFLOW);
s->data = e;
//2.将结点s链接到链式栈的头结点之后
s->next = S->next;
S->next = s;
return OK;
}
弹栈
Status Pop ( SeqStack &S, ElemType &e )
{
if ( S.next == NULL ) return ERROR; //空栈
//1.将栈顶数据元素赋值给e
e = S->next.data;
//2.删除栈顶元素
p = S->next;
S->next = p->next;
free(p);
return OK;
}
三、队列
与栈相似,其本质还是一种特殊的线性表。它规定只允许在表的一端进行插入操作另一端进行删除操作。由此,队列又称为FIFO(Frist In First Out 先进先出)的线性表。而允许插入操作的一端称为队尾,其插入操作称为入队;允许删除的一端称为队头(队首),其删除操作称为出队。
顺序队列
基本上同顺序表结构一致,只不过顺序队列要求必须从队尾插入元素,从队首删除元素。故基本的存储结构和插入删除操作可参考顺序表部分。
链式队列
存储结构
typedef struct LNode //结点结构
{
ElemType data; //数据域
struct LNode *next; //指针域
} LNode, *QueuePtr;
typedef struct LinkQueue //表结构
{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
} LinkQueue;
初始化操作
Status InitQueue( LinkQueue &Q )
{
Q.fornt = Q.rear = (LNode*) malloc ( sizeof(LNode) ); //申请头结点存储空间
if ( Q.front == NULL ) exit(OVERFLOW); //存储空间申请失败
Q.front->next = NULL;
return OK;
}
入队操作
Status EnQueue( LinkQueue &Q, ElemType e )
{
//1.申请新结点s的存储空间
s= (LNode *) malloc ( sizeof(LNode) );
if ( s == NULL ) exit(OVERFLOW); //存储空间申请失败
s->data = e;
//2.将结点s链接到队尾结点之后
Q.rear->next = NULL;
Q.rear = s; //更新队尾指针指向新的队尾
return OK;
}
出队操作
Status DeQueue( LinkQueue &Q, ElemType &e )
{
if ( Q.front == Q.rear ) return ERROR; //队列空
//1.将队首数据元素赋值给e
p = Q.front->next;
e = p->data;
//2.删除队头结点
Q.front->next = p->next;
if ( Q.rear == p ) Q.rear = Q.front; //如果已经是队尾,则更新队尾指针
free(p);
return OK;
}
循环队列
存储结构
#define MAXQSIZE 256 //队列最大的长度
typedef struct SeqQueue
{
ElemType *pBase; //动态存储空间的基地址
int front; //队头指针,指向队头元素
int rear; //队尾指针,指向队尾的下一个位置
}SeqQueue;
初始化操作
Status InitQueue( SeqQueue &Q )
{
Q.pBase = (ElemType *) malloc ( MAXQSIZE*sizeof(ElemType) ); //申请存储空间
if ( Q.pBase == NULL ) exit(OVERFLOW); //存储空间申请失败
Q.front = 0; //空队列,队头指向0号单元
Q.rear = 0; //空队列,队尾指向0号单元
return OK;
}
入队操作
Status EnQueue( SeqQueue &Q, ElemType e )
{
if ( (Q.rear+1)%MAXQSIZE == Q.front ) //队列满
return ERROR;
Q.pBase[rear] = e; //插入元素e作为新的队尾元素
Q.rear = (Q.rear+1)%MAXQSIZE; //队尾指针指向队尾元素的下一个位置
return OK;
}
出队操作
Status DeQueue( SeqQueue &Q, ElemType &e )
{
if ( Q.rear == Q.front ) return ERROR; //队列空
e = Q.pBase[front]; //将队头元素赋值给e
Q.front = (Q.front+1)%MAXQSIZE; //队头指针指向性的队头位置
return OK;
}