线性表:线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,n为表长。若用L命名线性表,则表示为:( a 1 a_1 a1, a 2 a_2 a2, a 3 a_3 a3… a i a_i ai, a i + 1 a_{i+1} ai+1…, a n a_n an)。
理解:
相同数据类型:每个元素所占的空间一样大。
有限序列:表中的元素一定有限个。例如:所有整数组成的序列不是线性表。
几个概念:
表头元素: a 1 a_1 a1,第一个数据元素
表尾元素: a n a_n an,最后一个数据元素
空表:n=0的线性表
线性表的逻辑特性(线性结构的特性):
线性表的特性:
顺序表:线性表的顺序存储。用一组地址连续的存储单元依次存储线性表中的数据元素,是的逻辑上相邻的两个元素在物理位置上也相邻。
顺序表的特点:
静态分配:
#include
#define MaxSize 10
typedef struct {
int data[MaxSize];
int length;
}ContiguousList;
/*
*Function:InitList
*Input:
arg1:ContiguousList &cl 要初始化的顺序表
*/
void InitList(ContiguousList &cl){
for (int i=0;i<MaxSize;i++){
cl.data[i]=0;
}
cl.length=0;
}
基本步骤:
1.判断插入位置是否合法,不合法返回false,插入失败。
2.将顺序表中的第i个元素及以后的所有元素向后移动一个位置,cl.data[j]=cl.data[j-1]
3.腾出空位置插入新元素e,cl.data[i-1]=e。
4.线性表长度+1,length++。
/*
*Function:ListInsert
*Input:
arg1:ContiguousList &cl 要插入的元素的顺序表
arg2:int i 要插入元素在顺序表中的位置
arg3:int e 要插入元素的值
*/
bool ListInsert(ContiguousList &cl,int i,int e){
if(i<1||i>cl.length+1){
printf("The scope of I is invalid");
return false;
}
if(cl.length>MaxSize){
printf("ContiguousList is full");
return false;
}
for (int j=cl.length;j>=i;j--){
cl.data[j]=cl.data[j-1];
}
cl.data[i-1]=e;
cl.length++;
return true;
}
时间复杂度分析:
最好情况:在表尾插入(i=n+1),元素后移语句不执行,时间复杂度为O(1)。
最坏情况:在表头插入(i=1),元素后移语句将执行n次,时间复杂度为O(n)。
平均情况:
i的范围:i=1,2,3…length+1;
每个位置上插入结点的概率为: P i P_i Pi= 1 n + 1 {1} \over {n+1} n+11;
i=1 循环执行n次
i=2 循环执行n-1次
i=3 循环执行n-2次
…
i=n+1 循环执行0次
所以移动结点的平均次数为:
∑ i = 1 n + 1 P i ( n − i + 1 ) \sum_{i=1}^{n+1}{P_i}(n-i+1) ∑i=1n+1Pi(n−i+1)= ∑ i = 1 n + 1 1 n + 1 ( n − i + 1 ) \sum_{i=1}^{n+1}{{1} \over {n+1}}(n-i+1) ∑i=1n+1n+11(n−i+1)= 1 n + 1 {1} \over {n+1} n+11 ∑ i = 1 n + 1 ( n − i + 1 ) \sum_{i=1}^{n+1}(n-i+1) ∑i=1n+1(n−i+1)= 1 n + 1 {1} \over {n+1} n+11 n ( n + 1 ) 2 {n(n+1)}\over{2} 2n(n+1)= n 2 {n}\over{2} 2n
基本步骤:
1.判断所删除元素位置i是否合法,不合法返回false,删除失败。
2.将被删除元素的值赋给e,e=cl.data[i]。
3.将i位置之后的元素向前移,cl.data[j-1]=cl.data[j]。
4.线性表长度-1,length–。
/*
*Function:ListDelete
*Input:
arg1:ContiguousList &cl 要删除的元素的顺序表
arg2:int i 要删除元素在顺序表中的位置
arg3:int &e 返回要删除元素的值
*/
bool ListDelete(ContiguousList &cl,int i,int &e){
if (i<1||i>cl.length){
printf("The scope of I is invalid");
return false;
}
e=cl.data[i-1];
for (int j=i;j<cl.length;j++){
cl.data[j-1]=cl.data[j];
}
cl.length--;
return true;
}
时间复杂度分析:
最好情况:删除表尾元素(i=n),元素后移语句不执行,时间复杂度为O(1)。
最坏情况:删除表头元素(i=1),元素后移语句将执行n-1次,时间复杂度为O(n)。
平均情况:
i的范围:i=1,2,3…length;
每个结点被删除的概率为: P i P_i Pi= 1 n {1} \over {n} n1;
i=1 循环执行n-1次
i=2 循环执行n-2次
i=3 循环执行n-3次
…
i=n 循环执行0次
所以移动结点的平均次数为:
∑ i = 1 n P i ( n − i ) \sum_{i=1}^{n}{P_i}(n-i) ∑i=1nPi(n−i)= ∑ i = 1 n 1 n ( n − i ) \sum_{i=1}^{n}{{1} \over {n}}(n-i) ∑i=1nn1(n−i)= 1 n {1} \over {n} n1 ∑ i = 1 n ( n − i ) \sum_{i=1}^{n}(n-i) ∑i=1n(n−i)= 1 n {1} \over {n} n1 n ( n − 1 ) 2 {n(n-1)}\over{2} 2n(n−1)= n − 1 2 {n-1}\over{2} 2n−1
/*
*Function:getElemByLocation
*Input:
arg1:ContiguousList &cl 要查找元素的顺序表
arg2:int i 要查找元素在顺序表中的位置
*/
int getElemByLocation(ContiguousList cl,int i){
return cl.data[i-1];
}
时间复杂度分析:
顺序表的各个元素在内存中连续存放,因此可以根据起始地址和数据元素大小直接找到第i个元素,所以时间复杂度为O(1)。
/*
*Function:getElemByValue
*Input:
arg1:ContiguousList &cl 要查找元素的顺序表
arg2:int e 要查找元素的值
*Return: 返回查找值在顺序表中的次序,返回0则查找失败。
*/
int getElemByValue(ContiguousList cl,int e){
for (int i=0;i<cl.length;i++){
if (cl.data[i]==e){
return i+1;
}
}
return 0;
}
时间复杂度分析:
最好情况:查找表头元素,仅比较一次,时间复杂度为O(1)。
最坏情况:查找表尾元素(或者不存在),需要比较n次,时间复杂度为O(n)。
平均情况:
i的范围:i=1,2,3…length;
每个元素被查找的概率为: P i P_i Pi= 1 n {1} \over {n} n1;
i=1 循环执行1次
i=2 循环执行2次
i=3 循环执行3次
…
i=n 循环执行n次
所以移动结点的平均次数为:
∑ i = 1 n P i ∗ i \sum_{i=1}^{n}{P_i}*i ∑i=1nPi∗i= ∑ i = 1 n 1 n ∗ i \sum_{i=1}^{n}{{1} \over {n}}*i ∑i=1nn1∗i= 1 n {1} \over {n} n1 n ( n + 1 ) 2 {n(n+1)}\over{2} 2n(n+1)= n + 1 2 {n+1}\over{2} 2n+1
/*
*Function:getLength
*Input:
arg1:ContiguousList &cl 要求取长度的顺序表
*Return:顺序表长度
*/
int getLength(ContiguousList &cl){
return cl.length;
}
/*
*Function:PrintList
*Input:
arg1:ContiguousList cl 要打印的顺序表
*/
void PrintList(ContiguousList cl){
for (int i=0;i<cl.length;i++){
printf("第%d个元素为:%d\n",i+1,cl.data[i]);
}
}
/*
*Function:isEmpty
*Input:
arg1:ContiguousList cl 要判断的顺序表
*Return:若表为空返回true,否则返回false
*/
bool isEmpty(ContiguousList cl){
if (cl.length==0)
{
return true;
}else{
return false;
}
}
动态分配:
#include
#include
#define InitSize 5 //默认最大长度
typedef struct {
int *data; //指示动态分配数组的指针
int MaxSize;
int length;
}ContiguousList;
//初始化
void InitList(ContiguousList &cl){
cl.data=(int *)malloc(InitSize*sizeof(int));
if(!cl.data)
exit(-1);
cl.length=0;
cl.MaxSize=InitSize;
}
/*
*Function:IncreaseSize
*Input:
arg1:ContiguousList &cl 要动态分配空间的顺序表
arg2:int len 要动态增加的长度
*/
void IncreaseSize(ContiguousList &cl,int len){
int *p=cl.data;
cl.data=(int *)malloc((cl.MaxSize+len)*sizeof(int));
for (int i=0;i<cl.length;i++){
cl.data[i]=p[i];
}
cl.MaxSize=cl.MaxSize+len;
free(p);
}
/*
*Function:ListInsert
*Input:
arg1:ContiguousList &cl 要插入的元素的顺序表
arg2:int i 要插入元素在顺序表中的位置
arg3:int e 要插入元素的值
*/
bool ListInsert(ContiguousList &cl,int i,int e){
if(i<1||i>cl.length+1){
printf("The scope of I is invalid");
return false;
}
if(cl.length>=cl.MaxSize){
IncreaseSize(cl,10);
}
for (int j=cl.length;j>=i;j--){
cl.data[j]=cl.data[j-1];
}
cl.data[i-1]=e;
cl.length++;
return true;
}
/*
*Function:getElemByValue
*Input:
arg1:ContiguousList cl 要查找元素的顺序表
arg2:int e 要查找元素的值
*Return: 返回查找值在顺序表中的次序,返回0则查找失败。
*/
int getElemByValue(ContiguousList cl,int e){
for (int i=0;i<cl.length;i++){
if (cl.data[i]==e){
return i+1;
}
}
return 0;
}
/*
*Function:getElemByLocation
*Input:
arg1:ContiguousList cl 要查找元素的顺序表
arg2:int i 要查找元素在顺序表中的位置
*Return: 返回该数值在顺序表中的序号
*/
int getElemByLocation(ContiguousList cl,int i){
if (i<1||i>cl.length){
printf("The scope of I is invalid");
return false;
}
return cl.data[i-1];
}
/*
*Function:getElemByLocation
*Input:
arg1:ContiguousList cl 要删除元素的顺序表
arg2:int i 要删除元素在顺序表中的位置
arg3:int e 要删除元素值
*/
bool ListDelete(ContiguousList &cl,int i,int &e){
if (i<1||i>cl.length){
printf("The scope of I is invalid");
return false;
}
e=cl.data[i-1];
for (int j=i;j<cl.length;j++){
cl.data[j-1]=cl.data[j];
}
cl.length--;
return true;
}
/*
*Function:PrintList
*Input:
arg1:ContiguousList cl 要打印的顺序表
*/
void PrintList(ContiguousList cl){
for (int i=0;i<cl.length;i++){
printf("第%d个元素为:%d\n",i+1,cl.data[i]);
}
}
/*
*Function:getLength
*Input:
arg1:ContiguousList cl 要求取长度的顺序表
*/
int getLength(ContiguousList cl){
return cl.length;
}
/*
*Function:isEmpty
*Input:
arg1:ContiguousList cl 要判断的顺序表
*Return:若表为空返回true,否则返回false
*/
bool isEmpty(ContiguousList cl){
if (cl.length==0)
{
return true;
}else{
return false;
}
}
/*
*Function:DestroyList
*Input:
arg1:ContiguousList cl 要销毁的顺序表
*/
bool DestroyList(ContiguousList &cl){
if(cl.data){
free(cl.data);
cl.data=NULL;
cl.length=0;
cl.MaxSize=0;
}else{
return false;
}
return true;
}
与静态顺序表相比,动态顺序表的操作相差并不大,不再赘述,只是多了动态分配空间的IncreaseSize()函数和Destroy()函数。
单链表:线性表的链式存储,通过任意一组存储单元存储线性表中的数据元素。
单链表的结点结构:
单链表的特点:
头结点:为了操作方便,在第一个结点之前附加结点,这个结点不存放任何数据。
头指针:用于标识一个单链表。
如果单链表没有头结点,那么头指针指向第一个结点。
如果单链表有头结点, 那么头指针指向头结点。
特别地,当头指针为null时,表示一个空表。
引入头结点的优点:
1.由于第一个数据结点的位置被存放在头结点的指针域,所以在链表的第一个位置上的操作和在表的其他位置一致,无需进行特殊操作。
2.无论链表是否为空,头指针都指向头结点的非空指针,空表与非空表的处理得到了同一。
基本步骤:
1.分配一个头结点,data域不存放任何数据。
2.让头指针L指向这个头结点。
3.因为是空表,让头结点指向null。
typedef struct LNode{
int data;
LNode *next;
}LNode,*LinkList;
/*
*Function:InitList
*Input:
arg1:LinkList &L 要初始化的链表
*/
bool InitList(LinkList &L){
L=(LNode *)malloc(sizeof(LNode));
if (L==NULL){
return false;
}
L->next=NULL;
return true;
}
基本步骤:
1.设置指针s。LNode *s;
2链表L指向NULL。L->next=NULL;
3.给指针s分配空间。s=(LNode *)malloc (sizeof(LNode));
4.让s的data域为插入结点值。s->data=data;
5.s的next域指向L的next。s->next=L->next;
6.L的next指向s。L->next=s;
/*
*Function:List_HeadInsert
*Description:头插法建立单链表
*Input:
arg1:LinkList &L 要建立的单链表
arg2:int length 建立单链表的长度
*/
LinkList List_HeadInsert(LinkList &L,int length){
int data;
L=(LinkList)malloc(sizeof(LNode));
LNode *s;
L->next=NULL;
while(length>0){
printf("请输入第%d个结点的值:",length);
scanf("%d",&data);
s=(LNode *)malloc (sizeof(LNode));
s->data=data;
s->next=L->next;
L->next=s;
length--;
}
return L;
}
时间复杂度分析:插入一个结点,循环一次,插入n个结点循环n次。所以T(n)=O(n);
基本步骤:
1.分配指针s、r,s指向插入头结点,r用于指向表尾。LNode *s,*r=L;
2.给s分配空间。s=(LNode *)malloc (sizeof(LNode));
3.s的值为插入结点值。s->data=data;
4.指针r指向s。r->next=s;
5.指针r指向表尾。r=s;
/*
*Function:List_TailInsert
*Description:尾插法建立单链表
*Input:
arg1:LinkList &L 要建立的单链表
arg2:int length 建立单链表的长度
*/
LinkList List_TailInsert(LinkList &L,int length){
int data;
int i=0;
L=(LinkList)malloc(sizeof(LNode));
LNode *s,*r=L; //指向头结点
while(i<length){
printf("请输入第%d个结点的值:",i+1);
scanf("%d",&data);
s=(LNode *)malloc (sizeof(LNode));
s->data=data;
r->next=s;
r=s;
i++;
}
r->next=NULL;
return L;
}
时间复杂度分析:插入一个结点,循环一次,插入n个结点循环n次。所以T(n)=O(n);
基本步骤:
1.设置指针p并指向第一个结点。LNode *p=L->next;
2.向后依次遍历。p=p->next;
/*
*Function:GetElemByLocation
*Description:按位查找
*Input:
arg1:LinkList &L 要查找的链表
arg2:int i 要查找元素的位置
*Return:
若i无效,返回NULL;
若i为0,返回头结点;
*/
LNode* GetElemByLocation(LinkList L,int i){
int j=1;
LNode *p=L->next;
if (i==0){
return L;
}
if (i<1){
return NULL;
}
while (p&&j<i){
p=p->next;
j++;
}
printf("第%d个结点的值为:%d\n",i,p->data);
return p;
}
时间复杂度分析:
每个元素被查找的概率为: P i P_i Pi= 1 n {1} \over {n} n1;
i=1 循环执行0次
i=2 循环执行1次
i=3 循环执行2次
…
i=n 循环执行n-1次
所以结点平均查找概率为:
∑ i = 1 n P i ∗ i \sum_{i=1}^{n}{P_i}*i ∑i=1nPi∗i= ∑ i = 1 n 1 n ∗ ( i − 1 ) \sum_{i=1}^{n}{{1} \over {n}}*{(i-1)} ∑i=1nn1∗(i−1)= 1 n {1} \over {n} n1 n ( n − 1 ) 2 {n(n-1)}\over{2} 2n(n−1)= n − 1 2 {n-1}\over{2} 2n−1
所以T(n)=O(n)
基本步骤:
1.设置指针p并指向链表L的第一个结点。
2.由后往前比较结点值。
/*
*Function:GetElemByValue
*Description:按值查找
*Input:
arg1:LinkList &L 要查找的链表
arg2:int e 要查找元素的值
*Return:
若查找不到,返回NULL;
*/
LNode* GetElemByValue(LinkList L,int e){
int location=1;
LNode *p=L->next;
while (p!=NULL&&p->data!=e){
p=p->next;
location++;
}
printf("值为%d的结点的序号为:%d\n",e,location);
return p;
}
时间复杂度:T(n)=O(n)
基本步骤:
1.设置p指针用于指向i-1位置的结点。 LNode *p;
2.利用函数GetElemByLocation找到第i-1个结点。p=GetElemByLocation(L,i-1);
3.初始化s,使s的data域为插入元素值。s->data=e;
4.s的next域指向i-1的next域(即p的next域,成为第i个结点)。s->next=p->next;
5.p的next指向s。再次形成链表。p->next=s;
/*
*Function:ListInsertRearNode
*Description:在i位置插入结点
*Input:
arg1:LinkList &L 要插入的链表
arg2:int i 要插入的位置
arg3:int e 要插入的元素值
*/
bool ListInsertByLocation(LinkList &L,int i,int e){
if (i<1){
return false;
}
LNode *p;
p=GetElemByLocation(L,i-1);
if(p==NULL){
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
时间复杂度:T(n)=O(n)
时间开销主要花费在p=GetElemByLocation(L,i-1)上,如果在给定结点后直接插入,则时间复杂度为O(1)。
1.初始化s,将s插入到p前面。s->next=p->next; p->next=s;
2.交换数据部分。s->data=p->data;p->data=e;
/*
*Function:ListInsertRearNode
*Description:将某个结点进行前插操作
*Input:
arg1:LinkList &L 要插入的链表
arg2:LNode *s 要插入的结点
arg3:LNode *p 将s插入到p前面
*/
bool ListInsertRearNode(LinkList &L,LNode *s,LNode *p){
s->next=p->next;
p->next=s;
int temp=p->data;
p->data=s->data;
s->data=temp;
return true;
}
基本步骤:
1.通过结点p找到第i-1个结点的位置。p=GetElemByLocation(L,i-1);
2.分配结点q,他是第i-1个结点的后继结点。LNode *q=p->next;
3.取值e保留删除结点值。e=q->data;
4.使得p结点跳过q结点指向q的后继结点。p->next=q->next;
5.释放q结点。free(q);
/*
*Function:ListDeleteByLocation
*Description:按照位序删除结点
*Input:
arg1:LinkList &L 要删除结点的链表
arg2:int i 要删除的位置
arg3:int e 要删除的元素值
*Return:
int e :删除的元素值
若成功,返回true。失败返回false。
*/
bool ListDeleteByLocation(LinkList &L,int i,int &e){
if (i<1){
return false;
}
LNode *p;
int j=0;
p=GetElemByLocation(L,i-1);
if(p==NULL){
return false;
}
if(p->next==NULL){
return false;
}
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}
时间复杂度分析:
每个元素被删除的概率为: P i P_i Pi= 1 n {1} \over {n} n1;
i=1 循环执行0次
i=2 循环执行1次
i=3 循环执行2次
…
i=n 循环执行n-1次
所以结点平均删除概率为:
∑ i = 1 n P i ∗ i \sum_{i=1}^{n}{P_i}*i ∑i=1nPi∗i= ∑ i = 1 n 1 n ∗ ( i − 1 ) \sum_{i=1}^{n}{{1} \over {n}}*{(i-1)} ∑i=1nn1∗(i−1)= 1 n {1} \over {n} n1 n ( n − 1 ) 2 {n(n-1)}\over{2} 2n(n−1)= n − 1 2 {n-1}\over{2} 2n−1
所以T(n)=O(n)
基本步骤:
1.分配结点q为被删结点p的后继结点。LNode *q=p->next;
2.使得p的值为其后继结点的值。p->data=p->next->data;
3.使得p指向其后面的后面(把原本的p结点跳过)。p->next=q->next;
4.释放q结点。free(q);
/*
*Function:ListDeleteByNode
*Description:按照结点删除结点
*Input:
arg1:LNode &p 要删除的结点
*Return:
若成功,返回true。失败返回false。
*/
bool ListDeleteByNode(LNode *p){
if(p==NULL){
return false;
}
LNode *q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
return true;
}
时间复杂度:
传入结点,无需遍历,所以时间复杂度T(n)=O(1)。
注意:如果传入的结点为表尾结点,则p->next->data会报空指针异常,所以此代码不能删除表尾结点,若要删除需要从头开始遍历。
由此我们可以看出单链表的缺点:无法逆向检索
/*
*Function:getLength
*Description:求表长
*Input:
arg1:LinkList L
*Return:
返回表长
*/
int getLength(LinkList L){
int length=0;
LNode *p=L->next;
while (p!=NULL)
{
p=p->next;
length++;
}
return length;
}
/*
*Function:ListPrint
*Description:打印链表
*Input:
arg1:LinkList &L 要打印的链表
*/
void PrintList(LinkList L){
LNode *p=L;
p=p->next;
int i=1;
while(p->data!=NULL){
printf("第%d个结点的值为:%d\n",i,p->data);
p=p->next;
i++;
}
}
typedef struct LNode{
int data;
LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L=NULL;
return true;
}
双链表:在单链表的基础上每个指针附设一个结点,指向前面的结点。
特点:
双链表为空:L->prior=L;L->next=L;
typedef struct DNode{
int data;
struct DNode *prior,*next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
L=(DNode *)malloc(sizeof (DNode));
if(L==NULL){
return false;
}
L->next=NULL;
L->prior=NULL;
return true;
}
基本步骤:
1.s结点指向p结点的后继结点。s->next=p->next;
2.判断是否为表尾结点,若果不是还需要让p结点的前驱结点指向s。p->next->prior=s;
3.s的前驱结点指向p。s->prior=p;
4.p的后继结点指向s。p->next=s;
/*
*Function:InsertNextDNode
*Description:在p结点之后插入s结点
*Input:
arg1:DNode *p
arg2:DNode *s 插入的结点
*/
bool InsertNextDNode(DNode *p,DNode *s){
if(p==NULL && s==NULL){
return false;
}
s->next=p->next;
if(p->next!=NULL){
p->next->prior=s;
}
s->prior=p;
p->next=s;
return true;
}
时间复杂度:T(n)=O(1)
基本步骤:
1.找到p结点的后继结点。DNode *q=p->next;
2.判断是否有后继结点,没有则无法删除。return false;
3.p结点指向q的后继结点。p->next=q->next;
4.q结点是不是最后一个结点,如果不是连接q的前驱结点指向p。q->next->prior=p;
5.释放q结点。free(q);
/*
*Function:DeleteNextDNode
*Description:删除p结点的后继结点
*Input:
arg1:DNode *p
*/
bool DeleteNextDNode(DNode *p){
if(p==NULL){
return false;
}
DNode *q=p->next;
if (q==NULL){
return false;
}
p->next=q->next;
if(q->next!=NULL){
q->next->prior=p;
}
free(q);
return true;
}
时间复杂度:T(n)=O(1)
循环单链表:表中最后一个结点的指针不是NULL,而是改为指向头结点而形成的一个环。
特点:循环双链表:
1.表头结点的prior指针指向表尾结点。
2.表尾结点的next指向头结点。
从逻辑结构来看:二者都属于线性表,都是线性结构。
从存储结构来看:
顺序表
链表
从基本操作来看:
1.初始化操作:
2.增加、删除操作:
3.查找操作:
按位查找:
顺序表:O(1)
链表:O(n)
按值查找:
顺序表:O(n)
链表:O(n)
1.基于存储的考虑:当难以估计线性表的长度或者存储规模时,适宜采用链表。
2.基于运算的考虑:
若是经常按照序号查找数据元素,适宜采用顺序表。
若是经常进行插入删除操作,适宜采用链表。