線性表是具有相同數據類型的n(n≥0)個數據元素的有限序列,n爲表長,當n=0時該線性表是一個空表。
邏輯特性:除表頭元素外,每個元素有且僅有一個直接前驅;除最後一個元素外,每個元素有且僅有一個直接後繼。
線性表基本操作的實現取決於採用哪種存儲結構。
線性表的順序表示
線性表的順序存儲又稱順序表。
線性表中元素的位序是從1開始的,而數組中元素的下標是從0開始的。
線性表的數據類型描述:
#define MAXSIZE N //定義線性表的最大長度
typedef struct SqList{
ElemType data[MAXSIZE];
int length;
}SqList;
數組可以是靜態分配的,也可以是動態分配的。
動態分配時不需要爲線性表一次性地劃分所有空間
#define InitSize 100 //表長度的初始定義
typedef struct SqList{
ElemType *data;
int MaxSize,length;
}SqList;
SqList L;
//c初始動態分配
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
//c++
L.data=new ElemType[InitSize];
順序表最主要的特點是隨機訪問,即通過首地址和元素序號可在時間內找到指定元素。
順序表的存儲密度高,每個結點只存儲數據元素。
順序表邏輯上相鄰的元素在物理上也相鄰,插入和刪除操作需要移動大量元素。
(1)插入
//在順序表的第i個位置插入e
bool ListInsert(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1){ //插入位置無效
return false;
}
if(L.length>=MaxSize){ //當前存儲空間已滿,不能插入
return false;
}
for(int j=L.length;j>i;j--){
L.data[j]=L.data[j-1];
}
L.data[i-1]=e;
L.length++;
return true;
}
*線性表元素位序從1開始,數組從0開始
平均時間複雜度
(2)刪除
//刪除順序表L第i個位置的元素
//若成功返回true,並將被刪除的元素引用變量e返回
bool ListDelete(SqList &L,int i,ElemType &e){
if(i<1||i>L.length){
return false;
}
e=L.data[i-1];
for(int j=i;j<L.length;j++){
L.data[i-1]=L.data[i];
}
L.length--;
return true;
}
平均時間複雜度爲
(3)按值查找
//查找表中第一個值爲e的元素,並返回它在順序表中的位置
int LocateElem(SqList &L,ElemType e){
int i;
for(i=0;i<L.length;i++){
if(L.data[i]==e){
return i+1;
}
}
return 0;
}
線性表的鏈式表示
鏈式存儲線性表時,不需要使用地址連續的存儲單元,即它不要求邏輯上相鄰的兩個元素在物理位置上也相鄰。
線性表的鏈式存儲又稱單鏈表。對每個鏈表結點,除存放元素自身的信息外,還需要存放一個指向其後繼的指針。
typeof struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
由於單鏈表的元素是離散地分佈在存儲空間中的,所以單鏈表是非隨機存取的存儲結構,即不能直接找到表中某個特定的結點。
通常用頭指針來標識一個單鏈表,頭指針爲NULL時表示一個空表。爲操作方便,在單鏈表第一個結點之前附加一個結點,稱爲頭結點。頭結點的數據域可以不設任何信息,也可以記錄表長等相關信息。頭結點的指針域指向線性表的第一個元素結點。
不管帶不帶頭結點,頭指針始終指向鏈表的第一個結點,而頭結點是帶頭結點的鏈表中的第一個結點,結點內通常不存儲信息。
(1)尾插法建立單鏈表
//從表頭到表尾正向建立單鏈表L
LinkList List_TailInsert(LinkList &L){
ElemType x;
L=(LinkList)malloc(sizeof(LNode));
LNode *s,*r=L; //r爲表尾指針
cin>>x;
while(x!=-1){ //輸入-1表示結束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //r指向新的表尾結點
cin>>x;
}
r->next=NULL; //尾結點指針置空
return L;
}
時間複雜度
(2)按序號查找結點值
//本算法取出單鏈表L(帶頭結點)中第i個位置的結點指針
LNode *GetElem(LinkList L,int i){
int j=1;
LNode *p=L->next; //頭結點指針賦給p
if(i==0){
return L; //返回頭結點
}
if(i<0){ //i無效
return NULL;
}
while(p&&j<i){
p=p->next;
j++;
}
return p; //返回第i個結點的指針,若i大於表長,p=NULL,直接返回p即可
}
時間複雜度
(3)按值查找表結點
LNode *LocalElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL&&p->data!=e){
p=p->next;
}
return p; //找到後返回該結點指針,否則返回NULL
}
時間複雜度
(4)插入結點
先檢查插入位置的合法性,然後找到待插入位置的前驅結點,即第i-1個結點,再在其後插入新結點。
p=GetElem(L,i-1);
s->next=p->next;
p->naxt=s;
算法的時間開銷主要在查找第i-1個元素,時間複雜度爲,若在給定的結點後面插入新結點,時間複雜度僅爲
(5)刪除結點
先檢查刪除位置的合法性,然後查找表中第i-1個結點,再將第i個結點刪除
p=GetElem(L,i-1);
q=p->next;
p->next=q->next;
free(q);
算法的時間開銷主要爲查找,時間複雜度爲
若要刪除結點*p,可以通過將其後繼結點的值賦予自身,然後刪除其後繼結點來實現,使得時間複雜度爲
q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
(6)求表長
求表長即計算單鏈表中數據結點(不含頭結點)的個數。時間複雜度爲
雙鏈表
雙鏈表結點中有兩個指針prior和next,分別指向前驅結點和後繼結點
typeof sturct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
雙鏈表“鏈”變化時也需要對prior指針做出修改
(1)插入:
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
(2)刪除:
p->next=q->next;
q->next->prior=p;
free(q);
循環鏈表
1.循環單鏈表
在循環單鏈表中,表尾結點*r的next域指向L,故表中沒有指針域爲NULL的結點,因此循環單鏈表的判空條件不是頭結點的指針是否爲空,而是它是否等於頭指針。
2.循環雙鏈表
爲空時,頭結點的prior域和next域都等於L
靜態鏈表
靜態鏈表藉助數組來描述線性表的鏈式存儲結構,這裏的指針是結點的相對地址(數組下標),也稱遊標。靜態鏈表也要預先分配一塊連續的內存空間。
#define MaxSize 50 //靜態鏈表的最大長度
typeof struct SLinkList{
ElemType data;
int next; //下一個元素的數組下標
}SLinkList[MaxSize];
靜態鏈表以next==-1作爲結束的標誌。靜態鏈表的插入、刪除操作與動態鏈表相同,只需要修改指針,不需要移動元素。
順序表和鏈表的比較
-
存取方式
順序表可以順序存放,也可以隨機存放。鏈表只能順序存取元素。 -
邏輯結構與物理結構
順序表邏輯結構相鄰的元素物理位置也相鄰。鏈式存儲邏輯上相鄰的元素物理位置不一定相鄰。 -
查找、插入和刪除操作
按值查找,順序表無序時,時間複雜度爲,順序表有序時,可採用折半查找,此時時間複雜度爲。鏈表時間複雜度。
按序號查找,順序表時間複雜度,鏈表的時間複雜度。
順序表插入、刪除操作,平均需要移動半個表長的元素。鏈表只需要修改相關結點的指針域。 -
空間分配
順序存儲在靜態存儲分配情形下,一旦存儲空間裝滿就不能擴充,若再加入新元素,則會出現內存溢出,因此需要預先分配足夠大的存儲空間,預先分配過大,有可能會導致順序表後部大量閒置。動態存儲分配雖然存儲空間可以擴充,但需要移動大量元素,導致操作效率降低,若內存中沒有更大塊的連續存儲空間,則會導致分配失敗。
鏈式存儲的結點空間只在需要時申請分配,只要內存有空間就可以分配,操作靈活、高效。
練習
(1)可將問題視爲將數組轉換爲數組。
先將,分別逆置得到,再將整個逆置得到。
(2)
void Reverse(int R[],int from,int to){
int i,temp;
for(i=0;i<(to-from+1)/2;i++){
temp=R[from+i];
R[from+i]=R[to-i];
R[to-i]=temp;
}
}
void Converse(int R[],int n,int p){
Reverse(R,0,p-1);
Reverse(R,p,n-1);
Reverse(R,0,n-1);
}
(3)時間複雜度,空間複雜度
(1)分別求A、B的中位數,設爲a、b
①若a=b,則a/b即爲所求中位數
②若a<b,則捨棄序列A中較小的一半和序列B中較大的一半,要求兩次捨棄的長度相等
③若a>b,則捨棄A中較大的一半和B中較小的一半,要求兩次捨棄的長度相等。
在保留的序列中,重複①②③,直到兩個序列均只含一個元素爲止,較小者即爲中位數
(2)
int MSearch(int A[],int B[],int n){
int s1=0,d1=n-1,s2=0,d2=n-1;
int m1,m2;
while(s1!=d1||s2!=d2){
m1=(s1+d1)/2;
m2=(s2+s2)/2;
if(A[m1]==B[m2]){
return A[m1];
}
if(A[m1]<B[m2]){
if((s1+d1)%2==0){ //若元素個數爲偶數
s1=m1;
d2=m2;
}
else{
s1=m1+1;
d2=m2;
}
}
else{
if((s2+d2)%2==0){
d1=m1;
s2=m2;
}
else{
d1=m1;
s2=m2+1;
}
}
}
return A[s1]<B[s2]?A[s1]:B[s2];
}
(3)時間複雜度,空間複雜度
(1)從前向後掃描數組元素,標記出一個可能成爲主元素的元素。最後重新計數,確認該元素是否爲主元素。
①選取數組第一個元素作爲主元素記爲c,依次掃描所給數組中的每個整數,記錄主元素出現的次數,若遇到的下一個整數仍等於c,則計數加1,否則計數減1;當技術減到0時,將遇到的下一個整數保存到c中,計數重置爲1,直到掃描完全部數組元素。
②判斷c中元素是否是真正的主元素。再次掃描該數組,統計c出現的次數,若大於n/2,則爲主元素,否則序列中不存在主元素。
(2)
int Major(int A[],int n){
int c,count=1;
c=A[0];
for(int i=0;i<n;i++){
if(A[i]==c){
count++;
}
else{
if(count>0){
count--;
}
else{
c=A[i];
count=1;
}
}
}
if(count>0){
for(i=count=0;i<n;i++){
if(A[i]=c){
count++;
}
}
}
if(count>n/2){
return c;
}
else{
return -1;
}
}
(3)時間複雜度,空間複雜度
(1)空間換時間。分配一個用於標記的數組B[n],用來記錄A中是否出現1~n中的數,B[0]對應1,B[n-1]對應n,初始化B中均爲0。
當A中n個數恰好爲1~n時返回n+1。當數組A中出現小於等於0或者大於n的值時,1~n中必會出現空餘位置。
從A[0]開始遍歷A,若0<A[i]<=n,則令B[A[i]-1]=1。遍歷結束後遍歷數組B,若找到滿足B[i]==0的下標i,循環結束,返回結果i+1;若B[i]均不爲0,循環結束後返回結果i+1。
(2)
int findMissMin(int A[],int n){
int *B;
B=(int *)malloc(sizeof(int)*n); //分配大小爲n的空間
memset(B,0,sizeof(int)*n);
for(int i=0;i<n;i++){
if(A[i>0]&&A[i]<=n){
B[A[i]-1]=1;
}
}
for(int i=0;i<n;i++){
if(B[i]==0){
break;
}
}
return i+1;
}
(3)時間複雜度,空間複雜度
將頭結點摘下,然後從第一個結點開始,依次插入到頭結點的後邊(頭插法建立單鏈表)
LinkList Reserve(LinkList L){
LNode *p,*r; //r爲p的後繼
p=L->next;
L->next=NULL;
while(p!=NULL){
r=p->next;
p->next=L->next;
L->next=p;
p=r;
}
return L;
}
時間複雜度,空間複雜度
(1)定義兩個指針變量p和q,初始時均指向頭結點的下一個結點,p指針沿鏈表移動;當p指針移動到第k個結點時,q指針開始與p指針同步移動;當p移動到最後一個結點時,q指針所指結點即爲倒數第k個結點。
(3)
typedef int ElemType;
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
int Search_k(LinkList list,int k){
LNode *p=list->next,*q=list->next;
int count=0;
while(p!=NULL){
if(count<k){
count++;
}
else{
q=q->next;
}
p=p->next;
}
if(count<k){
return 0;
}
else{
printf("%d",q->data);
return 1;
}
}
時間複雜度,空間複雜度
(1)①分別求出str1和str2所指兩個鏈表的長度m和n
②令p、q分別指向str1和str2的頭結點,若m≥n,則指針p先走,使p指向第m-n+1個結點,若m<n,則先使q指向第n-m+1個結點。
③反覆將p和q同步向後移動,當p、q指向同一位置時停止,即爲共同後綴的起始位置。
(2)
typedef struct Node{
char data;
struct Node *next;
}Node;
//求鏈表長度
int ListLen(Node *head){
int len=0;
while(head->next!=NULL){
len++;
head=head->next;
}
return len;
}
//找到共同後綴的起始地址
Node* findAddr(Node *str1,Node *str2){
int m,n;
Node *p,*q;
m=ListLen(str1);
n=ListLen(str2);
for(p=str1;m>n;m--){
p=p->next;
}
for(q=str2;m<n;n--){
q=q->next;
}
while(p->next!=NULL&&p->next!=q->next){
p=p->next;
q=q->next;
}
return p->next;
}
(3)時間複雜度,空間複雜度
(1)使用輔助數組記錄鏈表中已出現的數值。因爲|data|≤n,所以輔助數組的大小爲n+1,初值爲0。
依次掃描鏈表中各結點,同時檢查q[|data|]的值,若爲0則保留該結點,並令q[|data|]=1;否則將該結點刪除。
(2)
typedef struct Node{
int data;
struct Node *link;
}Node,*PNode;
(3)
void func(PNode h,int n){
PNode p=h,r;
int *q,m;
q=(int *)malloc(sizeof(int)*(n+1)); //申請n+1個位置的輔助空間
for(int i=0;i<n+1;i++){
*(q+1)=0;
}
while(p->link!=NULL){
m=p->link->data>0?p->link->data:-p->link->data;
if(*(q+m)==0){
*(q+m)=1;
p=p->link;
}
else{
r=p->link;
p->link=r->link;
free(r);
}
}
free(q);
}
(4)時間複雜度,空間複雜度