一、线性表概述
线性表(亦作顺序表)是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。
线性表可以说是最简单的数据结构,它的描述为:n个数据元素的有限序列。记为:L=(a1,a2,...,an)。按照存储结构它又可以分为顺序存储结构和链式存储结构。
(1)顺序存储结构
线性表的顺序存储结构是最简单最常用的数据结构:用一段连续地址依次存储表中的数据元素。(想象数组),下面实现的是线性表中的元素为整型的顺序存储结构,及它的主要运算:插入、删除和查找。
/*
线性表的顺序存储结构
底层数据结构:数组
*/
typedef int ELEMTYPE;
const int MAXSIZE = 100;
class Sqlist{
private:
ELEMTYPE data[MAXSIZE];
int length;
public://定义插入、删除、返回指定下标元素操作
Sqlist():length(0){}
~Sqlist(){}
void Insert(int index , ELEMTYPE e);//向下标index处插入元素e
bool Delete(int index);
ELEMTYPE getElem(int index);//返回下标index处元素
void printList();//打印整个list
};
void Sqlist::Insert(int index , ELEMTYPE e)
{
if(length>=MAXSIZE || index>length || index<0)
return;
for(int i=length-1 ; i>=index ; i--)//从index开始每个元素都向后移一位
data[i+1] = data[i];
data[index] = e;
length++;
}
bool Sqlist::Delete(int index)
{
if(index<0 || index>length-1 || length<=0)
return false;
for(int i=index ; i<length-1 ; i++)
data[i] = data[i+1];
data[length-1] = 0;//删除最后一个元素
length--;
return true;
}
ELEMTYPE Sqlist::getElem(int index)
{
if(index<0 || index>length-1 || length<=0)
return -1;
return data[index];
}
void Sqlist::printList()
{
if(length == 0){
cout<<"表为空!"<<endl;
return;
}
for(int i=0 ; i<length ; i++)
cout<<data[i]<<" ";
cout<<endl;
}
int main()
{
Sqlist list;
for(int i=0 ; i<6 ; i++)
list.Insert(i,i+3);
list.Insert(3,100);
list.Delete(0);
list.printList();
return 0;
}
线性表的顺序存储结构,在存取数据时复杂度O(1);在插入删除时复杂度O(n),需要多次移动数据。它比较适合元素个数不太变化,更多的是存取数据的应用。
优点:无须为元素之间的逻辑关系增加额外的存储空间,可以快速的存取表中任意位置的元素;
缺点:插入和删除需要移动大量的元素。
(2)链式存储结构
针对顺序存储结构的插入删除需要移动大量元素,提出链式存储结构,每个结点存储元素信息外,还要存储它的后继元素的地址。链表中第一个结点的存储位置叫做头指针。有时为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个头结点,它的数据域可以不存储任何信息,也可以存储线性表长度等附加信息。
头指针和头结点的异同:
下面实现元素类型为整形的链式存储结构和它的基本操作:插入、删除、查找和表删除。
/*
线性表的链式存储结构
底层数据结构:结点
下标从1开始
*/
typedef int ELEMTYPE;
struct LinkNode
{
LinkNode():pNext(NULL){}
~LinkNode(){}
ELEMTYPE data;
LinkNode *pNext;
};
class Linklist{
private:
LinkNode *pHead;//头指针,存储链表头结点地址
public://定义插入、删除、返回指定下标元素操作
Linklist(){pHead = new LinkNode;}//头结点
~Linklist(){}
int ListLength();//返回链表长度
LinkNode* getElem(int i);//返回第i个节点的地址,不存在返回NULL
bool Insert(int i , ELEMTYPE e);//在第i个元素前面插入e
bool Delete(int i);//删除下标为index的结点,头结点不能删
void Append(ELEMTYPE e);//在尾部插入e
void HeadInsert(ELEMTYPE e);//在头部插入e
void destory();//删除整个表
void printList();//打印整个list
};
bool Linklist::Insert(int i , ELEMTYPE e)
{
if(i<1 || i>ListLength()+1)
return false;
//---得到第i-1个元素位置
LinkNode *pNode = getElem(i-1);
LinkNode *tempNode = new LinkNode;
tempNode->data = e;
tempNode->pNext = pNode->pNext;
pNode->pNext = tempNode;
return true;
}
bool Linklist::Delete(int i)
{
if(i<1 || i>ListLength())
return false;
//---得到第i-1个元素位置
LinkNode *pNode = getElem(i-1);
//---暂存第i个结点,并delete
LinkNode *tempNode = pNode->pNext;
pNode->pNext = tempNode->pNext;
delete(tempNode);
return true;
}
int Linklist::ListLength()
{
LinkNode *pNode = pHead->pNext;//第一个有效元素
int count = 0;
while(pNode != NULL){
pNode = pNode->pNext;
count++;
}
return count;
}
//---第0个元素就是头结点
LinkNode* Linklist::getElem(int i)
{
if(i<0 || i>ListLength())
return NULL;
LinkNode *pNode = pHead;
for(int k=1 ; k<=i ; k++)//循环i次
pNode = pNode->pNext;
return pNode;
}
//---尾插法插入元素
void Linklist::Append(ELEMTYPE e)
{
LinkNode *Tail = pHead;
while(Tail->pNext)
Tail = Tail->pNext;
LinkNode *tempNode = new LinkNode;
tempNode->data = e;
tempNode->pNext = Tail->pNext;
Tail->pNext = tempNode;
}
//---头插法插入元素
void Linklist::HeadInsert(ELEMTYPE e)
{
LinkNode *tempNode = new LinkNode;
tempNode->data = e;
tempNode->pNext = pHead->pNext;
pHead->pNext = tempNode;
}
void Linklist::printList()
{
LinkNode *pNode = pHead->pNext;
while(pNode){
cout<<pNode->data<<" ";
pNode = pNode->pNext;
}
cout<<endl;
}
void Linklist::destory()
{
LinkNode* pNode = pHead;
LinkNode* pTemp;
while(pNode){
pTemp = pNode;
pNode = pNode->pNext;
delete(pTemp);
}
pHead = pTemp = NULL;
}
int main()
{
Linklist list;
for(int i=1 ; i<6 ; i++)
list.Insert(i,i+3);
list.printList();
//list.destory();
//cout<<list.getElem(5);
return 0;
}
总结
(1)存储分配方式:顺序存储结构用一段连续的存储空间依次存储线性表元素,单链表用一组任意存储单元存放线性表元素。
(2)时间性能:顺序存储结构与单链表分别是:查找O(1) vs O(n),插入删除O(n) vs O(1)
(3)空间性能:顺序存储结构空间需要预分配,分大了浪费,分小了容易上溢;单链表不需预分配,只要有空间就可以分配。
二、单链表与单向循环链表
(1)单向循环链表
将上一节单链表中终端节点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相连的单链表称为单循环链表,简称循环链表。
其实相对于单链表(非循环),改变的仅仅只是判空条件而已,最后一个节点从pNode->next = NULL变为pNode->next = pHead,我们还可以在此基础上以O(1)的复杂度访问尾节点,只需要将头指针改为指向终端节点的尾指针Tail,这样Tail->data就是尾节点数据,Tail->next就是头结点,Tail->next->next就是第一个有数据节点。下面是实现:
<span style="font-size:12px;">//---单向循环链表
#define DATATYPE char
struct LINKLIST
{
LINKLIST():next(NULL){}
DATATYPE data;
LINKLIST *next;
};
void initList(LINKLIST* &head)
{
head = new LINKLIST;
head->next = head;//建立单向循环链表
}
void printList(LINKLIST *head)
{
LINKLIST *pNode = head->next;//第一个有数据节点
while(pNode != head){
cout<<pNode->data<<" ";
pNode = pNode->next;
}
cout<<endl;
}
//在已知结点之后插入一个结点
void Insert(LINKLIST *p ,DATATYPE inData)
{
if(NULL == p)
return;
LINKLIST *t = new LINKLIST;
t->data = inData;
t->next = p->next;
p->next = t;
}
//删除元素为c的结点
bool Delete(LINKLIST *head , DATATYPE c)
{
LINKLIST *q = head;
while(q->next!=head && q->next->data!= c){
q = q->next;
}
if(q->next == head)//空表或遍历结束未找到c
return false;
LINKLIST *t = q->next;
q->next = t->next;
delete(t);
return true;
}
//查找第i个元素
char getElem(LINKLIST *head ,int i)
{
LINKLIST *p = head->next;
int count = 1;
while(p!=head && count<i){
p = p->next;
count++;
}
if(count>i || p==head){
cout<<"未找到所查找的元素"<<endl;
return 0;
}
return p->data;
}
int main()
{
LINKLIST *pHead;
initList(pHead);
//---插入元素
Insert(pHead,'a');
Insert(pHead,'b');
Insert(pHead,'c');
Delete(pHead,'d');
//printList(pHead);
cout<<getElem(pHead,3);
free(pHead);
return 0;
}</span>
三、双向链表与双向循环链表
引入:
在单链表中,查找下一元素可以直接用next指针在O(1)内完成,而查找前驱则需O(n)完成,为了克服单向性这一缺点,提出了双向链表,顾名思义,就是数据本身具备了左边和右边的双向指针。双向链表相比较单向链表,主要有下面几个特点:
(1)在数据结构中具有双向指针,前驱与后继指针;
(2)可以正反双向遍历链表,存取数据较为方便;
(3)插入、删除数据的时候需要考虑前后方向;
由性质可以知道p->next->prior = p = p->prior->next ;
(1)双向链表
对于双向链表(非循环)与双向循环链表差异比较小,主要差异体现在判空条件与插入删除上,此时空链表pHead->next = pHead->piror = NULL,同时又由于非循环链表,头结点的prior=NULL,尾节点的next=NULL,因此插入和删除需要分情况讨论:在尾部插入和在链表中间插入,删除同理。
尾部处理需要处理两个指针域,链表中间处理需要处理四个指针域。
//---双向链表(非循环)
#define DATATYPE char
struct LINKLIST
{
LINKLIST():next(NULL),prior(NULL){}
DATATYPE data;
LINKLIST *prior;//前驱
LINKLIST *next;//后继
};
void initList(LINKLIST* &head)
{
//建立空的双向链表
head = new LINKLIST;
}
bool ListEmpty(LINKLIST *pHead)
{
if(NULL==pHead->next && NULL==pHead->prior)
return true;
return false;
}
void ListClear(LINKLIST *pHead)
{
LINKLIST* pNode = pHead->next;//第一个有效元素
LINKLIST* t;
while(pNode != NULL){
t = pNode;
pNode = pNode->next;
delete t;
}
pHead->next = pHead->prior = NULL;
}
void printList(LINKLIST *pHead)
{
LINKLIST *pNode = pHead->next;//第一个有数据节点
while(pNode != NULL){
cout<<pNode->data<<" ";
pNode = pNode->next;
}
cout<<endl;
}
//输出链表长度
int ListLength(LINKLIST *pHead)
{
LINKLIST *pNode = pHead->next;//第一个有效元素
int count = 0;
while(pNode != NULL){
pNode = pNode->next;
count++;
}
return count;
}
//得到第i个元素节点地址,如果i=0则返回头结点地址
LINKLIST* getElem(LINKLIST *pHead ,int i)
{
if(i<0 || i>ListLength(pHead))
return NULL;
LINKLIST* pNode = pHead;//头结点,第0个节点
for(int k=1 ; k<=i ; k++)//循环i次
pNode = pNode->next;
return pNode;
}
//在第i个结点之前插入一个结点
bool Insert(LINKLIST *pHead ,int i , DATATYPE inData)
{
if(i<1 || i>ListLength(pHead)+1)//至多在第lenght+1个节点之前插入节点
return false;
//---定位第i-1个节点
LINKLIST *pNode = getElem(pHead , i-1);
//---在第i-1个节点之后插入节点
LINKLIST* temp = new LINKLIST;
temp->data = inData;
//---双向链表(不循环)插入节点与循环链表作法不同
if(i==ListLength(pHead)+1){//在链表尾部插入
pNode->next = temp;
temp->prior = pNode;
}else{//在链表中间插入需要处理四个指针域
temp->next = pNode->next;
pNode->next->prior = temp;
temp->prior = pNode;
pNode->next = temp;//最后一步,否则断链
}
return true;
}
//删除第i个结点
bool Delete(LINKLIST *pHead , int i)
{
if(i<1 || i>ListLength(pHead))
return false;
//---定位第i-1个节点
LINKLIST *pNode = getElem(pHead , i-1);
//---双向链表(不循环)删除节点与循环链表作法不同
LINKLIST *temp = pNode->next;
if(i==ListLength(pHead)){//删除链表尾部节点
pNode->next = NULL;
}else{
temp->next->prior = pNode;
pNode->next = temp->next;
}
delete(temp);
return true;
}
int main()
{
LINKLIST *pHead;//头结点
initList(pHead);
//---插入元素
Insert(pHead,1,'a');
Insert(pHead,2,'b');
Insert(pHead,3,'c');
Insert(pHead,2,'d');
ListClear(pHead);
printList(pHead);
return 0;
}
(2)双向循环链表
//---双向循环链表
#define DATATYPE char
struct LINKLIST
{
LINKLIST():next(NULL),prior(NULL){}
DATATYPE data;
LINKLIST *prior;//前驱
LINKLIST *next;//后继
};
void initList(LINKLIST* &head)
{
//建立空的双向循环链表
head = new LINKLIST;
head->next = head->prior = head;
}
void printList(LINKLIST *head)
{
LINKLIST *pNode = head->next;//第一个有数据节点
while(pNode != head){
cout<<pNode->data<<" ";
pNode = pNode->next;
}
cout<<endl;
}
//输出链表长度
int ListLength(LINKLIST *pHead)
{
LINKLIST *pNode = pHead->next;//第一个有效元素
int count = 0;
while(pNode != pHead){
pNode = pNode->next;
count++;
}
return count;
}
//得到第i个元素节点地址,如果i=0则返回头结点地址
LINKLIST* getElem(LINKLIST *pHead ,int i)
{
if(i<0 || i>ListLength(pHead))
return NULL;
LINKLIST* pNode = pHead;//头结点,第0个节点
for(int k=1 ; k<=i ; k++)//循环i次
pNode = pNode->next;
return pNode;
}
//在第i个结点之前插入一个结点
bool Insert(LINKLIST *pHead ,int i , DATATYPE inData)
{
if(i<1 || i>ListLength(pHead)+1)//至多在第lenght+1个节点之前插入节点
return false;
//---定位第i-1个节点
LINKLIST *pNode = getElem(pHead , i-1);
//---在第i-1个节点之后插入节点
LINKLIST* temp = new LINKLIST;
temp->data = inData;
temp->next = pNode->next;
pNode->next->prior = temp;
temp->prior = pNode;
pNode->next = temp;//最后一步,否则断链
return true;
}
//删除第i个结点
bool Delete(LINKLIST *pHead , int i)
{
if(i<1 || i>ListLength(pHead))
return false;
//---定位第i-1个节点
LINKLIST *pNode = getElem(pHead , i-1);
//---删除第i个节点
LINKLIST *temp = pNode->next;
temp->next->prior = pNode;
pNode->next = temp->next;
delete(temp);
return true;
}
int main()
{
LINKLIST *pHead;//头结点
initList(pHead);
//---插入元素
Insert(pHead,1,'a');
Insert(pHead,2,'b');
Insert(pHead,3,'c');
Delete(pHead,4);
printList(pHead);
return 0;
}
三、静态链表
静态链表相当于是用一个数组来实现线性表的链式存储结构,大概结构图如下:
在静态链表中,每一个结点包含两部分内容,一部分是data(即有意义的数据),另一部分是cur(存储该元素下一个元素所在数组对应的下标)。
下标为0的结点中不包含有意义的数据,它的cur存储的是备用链表(即没有存储的数据的那些结点)第一个结点的下标。如下图所示,数组第一个元素的cur存放的是7。
数组最后一个元素的cur存放的是静态链表的第一个有实质意义的数据的结点下标,相当于头结点,当链表为空时,cur设定为0。上图链表从下标1开始于是小红旗的cur为1。
链表的最后一个元素,最后一个存放了数据的元素的cur一般存放0,表示它后面的结点为空了。见“庚”节点。
(1)结构
#define MAXSIZE 1000
typedef struct
{
int cur;
int data;
}SLListNode,SLList[MAXSIZE];
(2)初始化
void initSLList(SLList slist)
{
//---未使用链表首下标为1,其余位置串成一串
for(int i=0 ; i<MAXSIZE-1 ; i++)
slist[i].cur = i+1;
slist[MAXSIZE-1].cur = 0;//头节点,现在还没有数据
}
(3)插入和删除
静态链表插入操作要解决的是如何用在静态结构中模拟动态链表结构中的分配节点与释放节点。
为了辨明哪些节点未被使用,可以用游标链将所有未使用过的以及被删除的结点链起来形成一个备用链表,每当进行插入时,便可以从备用链表中获得一个结点作为待插入的新节点。因此在插入之前需要定义分配函数和释放函数以获取和释放存储空间。
注意到对于插入函数,头结点slist[Max-1]的cur=0相当于NULL,插入第一个节点,将会赋头结点的cur为第一个节点下标,并将第一个节点的cur赋值为0(相当于动态链表中的NULL),这个0是作为链表结束标识。
//参考上图,0存有7下标,当分配7空间后就将7的后继8赋值给0
int Malloc_SLL(SLList slist)
{
int i = slist[0].cur;//获取第一个备用的空闲下标
if(slist[0].cur)
slist[0].cur = slist[i].cur;//拿下一个空闲节点下标作为备用
return i;
}
//
void Free_SLL(SLList slist , int k)
{
//类似于头插法将空间插入备用链表中
slist[k].cur = slist[0].cur;
slist[0].cur = k;
}
//在链表的第k个元素(不一定是数组的第k个元素)之前插入新元素
bool ListInsert(SLList slist , int k , int data)
{
if(k<1 || k>ListLength(slist)+1)
return;
//---先将元素data存放在新分配空间
int newNodeIndex = Malloc_SLL(slist);
if(0 != newNodeIndex){//分配空间成功
slist[newNodeIndex].data = data;//将元素放在新空间内
//---找到第k-1个元素
int count = MAXSIZE-1;//头结点下标
for(int i=1;i<k;i++){//循环k-1次
int curNext = slist[count].cur;
count = curNext;
}
//---插入,修改cur
slist[newNodeIndex].cur = slist[count].cur;
slist[count].cur = newNodeIndex;
return true;
}
return false;
}
//删除链表的第k个元素
bool ListDelete(SLList slist , int k)
{
if(k<1 || k>ListLength(slist))
return false;
//---找到第k-1个元素
int count = MAXSIZE-1;//头结点
for(int i=1 ; i<k ; i++){//循环k-1次
int curNext = slist[count].cur;
count = curNext;
}
//---删除,修改cur
int kIndex = slist[count].cur;//第k个元素的下标
slist[count].cur = slist[kIndex].cur;//第k个元素的后继
Free_SLL(slist , kIndex);
return true;
}
//返回slist中元素的个数
int ListLength(SLList slist)
{
int count = 0;
int index = slist[MAXSIZE-1].cur;//头结点
while(0 != index){
index = slist[index].cur;
count++;
}
return count;
}
(4)总结
优点:在插入和删除操作,只需要移动游标,不需要移动元素,从而改进了顺序存储结构中该操作需要移动大量元素的缺点。
缺点:没有解决连续存储分配带来的MAXSIZE难以确定的问题,且失去了顺序存储结构随机存取的特性。