数据类型是一个值的集合和定义在这个值集上的一组操作的总称。
例如,C语言中的整形变量,其值集为某个区间上的整数(区间大小依赖于不同的机器),定义在其上的操作为加、减、乘、除和取模等算术运算。
高级程序语言中的数据类型可分为两类:
原子类型:原子类型的值不可再分,例如C语言中的基本类型(整形、实型、字符型和枚举型)、指针类型和空类型(void)。
结构类型:结构类型的值是由若干成分按照某种结构组成的,因此是可以分解的,并且它的成分可以是非结构的(例如整形数组),也可以是结构的(例如结构体数组)。
而我们口中常说的数据结构仅可以看作是“一组具有相同结构的值”,而数据结构这门课要学习的就是特殊的数据类型,即是其如何定义,以及其上的操作。
顺序表和链表的区别:
顺序表:逻辑关系上相邻的两个数据元素在物理位置上也相邻。因此可以随机存取表中任一数据元素,但是在做插入或删除操作时,需移动大量元素。
链表: 逻辑关系上相邻的两个数据元素在物理位置上不一定也相邻。每个结点都包含两部分:数据域+指针域,指针域用于存储后一个结点的地址,虽避免了顺序表结构所具有的弱点,但也失去了随机存取的特点,需要查找表中某一元素时,必须从头遍历。
顺序表
由于线性表的长度可变,且所需最大存储空间随问题不同而不同,则在C语言中可用动态内存分配的方式创建一维数组
数据结构的定义:
#define DEFAULT_SZ 3 //顺序表的初始长度
#define DEFAULT_INC 2 //每次增容的长度
typedef int DataType; //为了使表中数据元素类型修改方便
typedef struct SeqList
{
DataType* data; //存储空间基址,一个DataType类型的动态数组
int sz; //当前长度
int capacity; //当前容量
}SeqList, *pSeqList;
(1)顺序表的初始化:
void InitSeqList(pSeqList ps)
{
DataType *tmp = 0;
assert(ps);
tmp = (DataType *)calloc(DEFAULT_SZ, sizeof(DataType));
if (tmp == NULL)
{
return;
}
ps->data = tmp;
ps->capacity = DEFAULT_SZ;
ps->sz = 0;
}
(2)顺序表的销毁
void DestroySeqList(pSeqList ps)
{
assert(ps);
free(ps->data);
ps->data = NULL;
ps->capacity = 0;
ps->sz = 0;
}
(3)顺序表的打印
void ShowSeqList(pSeqList ps)
{
int i = 0;
assert(ps);
for (i=0; i<ps->sz; i++)
{
printf("%d ", ps->data[i]);
}
printf("\n");
}
(4)顺序表的头插和头删
在对动态开辟出的数组空间进行使用的时候,必须要先判断当前使用的空间大小,若空间已使用完,再进行元素的插入时就需要扩容,所以通过以下的函数来进行判断,并在需要扩容时完成扩容操作
static int is_expend(pSeqList ps)
{
DataType *tmp = 0;
assert(ps);
if (ps->capacity == ps->sz)
{
tmp = (DataType *)realloc(ps->data, (ps->capacity+DEFAULT_INC)*sizeof(DataType));
if (tmp != NULL)
{
ps->data = tmp;
ps->capacity += DEFAULT_INC;
}
else
{
return 0;
}
}
return 1;
}
头插和头删操作可以很直观的反应线性表在进行插入或者删除操作时的缺陷。
在头部插入,需要先将整个线性表整体向后“平移”一个元素大小的位置:
void PushBack(pSeqList ps, DataType d)
{
int i = ps->sz-1;
assert(ps);
if(is_expend(ps) == 0)//判断是否需要扩容,若if条件为真表示扩容失败
{
return;
}
//进行整体的向后平移
for (; i>=0; i--)
{
ps->data[i+1] = ps->data[i];
}
ps->data[0] = d;//将需要头插的元素放在头部
ps->sz++;
}
在头部删除,需要将整个线性表整体向前“平移”一个元素大小的位置,从而覆盖掉第数组第一个元素,实现删除操作:
void PopFront(pSeqList ps)
{
int i = 0;
assert(ps);
if(is_expend(ps) == 0)//判断是否需要扩容,若if条件为真表示扩容失败
{
return;
}
//进行整体的向前覆盖
for (i=0; i<ps->sz-1; i++)
{
ps->data[i] = ps->data[i+1];
}
ps->sz--;
}
一般情况下,在第i(1≤i≤n)个元素之前插入一个元素时,需将第n至第i(共n-i+1)个元素向后移动一个位置;删除第i(1≤i≤n)个元素时需将从第i+1至第n(共n-i)个元素依次向前移动一个位置。
链表
链表两种常见的维护方式:
头指针:创建一个能够指向结点类型的指针,用于维护整个链表
头结点:在第一个结点之前附设一个结点,称之为头结点,头结点的数据域可以不存储任何信息,也可存储如链表长度等类的附加信息,头结点的指针域存储指向第一个结点的指针
以下采取头指针的方式
数据结构的定义:
typedef int DataType;
typedef struct Node
{
DataType data; //数据域
struct Node *next; //指针域,指向下一个结点的地址
}Node, *pNode;
(1)链表的初始化
void InitLinkList(pNode* pplist)
{
assert(pplist);
*pplist = NULL;
}
因为是用头指针的形式维护,创建了一个pNode plist
指针,所以初始化的时候需要用pNode* pplist
接收,即Node** pplist
(2)创建结点
需要频繁开辟结点,所以将此操作封装成一个函数。故链表的每一个结点必须在堆上动态内存开辟,不能以创建变量的形式在函数内部(栈上)开辟,因为函数调用结束后,栈上开辟的空间就会销毁
pNode BuyNode(DataType d)
{
pNode newNode = (pNode)malloc(sizeof(Node));
if (NULL == newNode)
{
perror("BuyNode::malloc()");
return NULL;
}
newNode->data = d;
newNode->next = NULL;
return newNode;
}
(3)链表的打印
void PrintLinkList(pNode plist)
{
pNode cur = plist;
while (cur)
{
printf("%d--->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
链表的打印十分简单,这里特意拿出来的目的是要说明,在构造函数的时候,如果接收一个指针变量,并且在函数中要对其解引用,为了代码的安全性考虑,都需要进行断言判断接收的指针不为空,但是在用头指针方式维护的链表中,若链表为空,其值为空,但合法,所以不需断言。
(4)尾插和尾删
void PushBack(pNode *pplist, DataType d)
{
pNode cur = NULL;
assert(pplist);
if (*pplist == NULL)
{
*pplist = BuyNode(d);
}
else
{
cur = *pplist;
while (cur->next)
{
cur = cur->next;
}
cur->next = BuyNode(d);
}
}
void PopBack(pNode *pplist)
{
pNode cur = NULL;
pNode del = NULL;
assert(pplist);
if (*pplist == NULL)
{
return;
}
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
cur = *pplist;
while (cur->next->next)
{
cur = cur->next;
}
free(cur->next);
cur->next = NULL;
}
}
(5)头插和头删
头插看似没有进行链表的状态判断,实际上是把不同状态所实现的操作统一成了一种操作而已
void PushFront(pNode *pplist, DataType d)
{
pNode NewNode = BuyNode(d);
assert(pplist);
NewNode->next = *pplist;
*pplist = NewNode;
}
void PopFront(pNode *pplist)
{
assert(pplist);
if (*pplist == NULL)
{
return;
}
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
pNode del = *pplist;
*pplist = (*pplist)->next;
free(del);
del = NULL;
}
}
通过(4)(5)两个操作可以发现,在对使用头指针维护的链表进行操作的时候,必须要判断链表当前的状态:
(1)为空
(2)只有一个结点
(3)结点个数大于1
因为不同的状态要考虑到什么时候改变头指针的值,什么时候只需要对操作所涉及到链表结点的指向进行改变
(6)链表的销毁
void DestroyLinList(pNode *pplist)
{
pNode cur = NULL;
pNode del = NULL;
assert(pplist);
if (*pplist == NULL)
{
return;
}
else if ((*pplist)->next == NULL)
{
cur = *pplist;
free(cur->next);
cur->next = NULL;
}
else
{
cur = *pplist;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
*pplist = NULL;
}
链表的销毁注意不仅要把动态内存开辟的空间进行释放,而且要把头指针赋成NULL,避免访问已经释放的堆空间