資料結構與演算法(C語言) | 線性表(順序儲存、鏈式儲存)
線性表是最常用最簡單的線性結構
線性結構具有以下基本特徵:
線性結構是一個數據元素的有序(次序)集(處理元素有限)。若該集合非空,則
1)必存在唯一的一個“第一元素”;
2)必存在唯一的一個“最後元素”;
3)除第一元素之外,其餘每個元素均有唯一的前驅;
4)除最後元素之外,其餘每個元素均有唯一的後繼。
抽象資料型別(ADT)格式:
ADT 抽象資料型別名
Data
資料元素之間邏輯關係的定義
Operation
操作
endADT
一、線性表的順序儲存結構
用一組地址連續的儲存單元依次儲存線性表元素,線性表的這種機內表示稱作線性表的順序儲存結構或順序映像,簡稱順序表。
順序表的特點:
a.邏輯上相鄰的元素其物理位置也相鄰。
b.能按邏輯序號(位序)對元素進行隨機存取。
線性表順序儲存的結構程式碼
#define MAXSIZE 20
typedef int ElemType
typedef struct
{
ElemType data[MAXSIZE];
int length;//線性表長度
}
線性表的基本操作在順序表中的實現
InitList(&L) // 結構初始化
ListInsert(&L,i,e) // 插入元素
ListDelete(&L,I, &e) //
LocateElem(L,e, compare()) // 查詢
#include<iostream> using namespace std; #define LIST_INT_SIZE 100 template<class T> class List{ public: List(int size); ~List(); int Locate(T x); bool Full(); bool Empty(); bool Insert(int i,T x); bool Delete(int i); void input(); void output(); protected: T *data; //儲存空間基址 int listsize; int length; }; //構造一個空的順序表 template<class T> List<T>::List(int size){ listsize = size; length = 0; data = new T[listsize]; if(data == 0) { cout<<"儲存分配錯誤!"<<endl; exit(1); } } template<class T> List<T>::~List(){ delete[] data; } //定位 template<class T> int List<T>::Locate(T x) { int i; for(i=0; i<length ; i++) { if(data[i] == x) return i+1; } if(i == length) return -1; } //判定順序表是否滿了 template<class T> bool List<T>::Full(){ return (length==listsize) ? true : false; } //判空 template<class T> bool List<T>::Empty(){ return (length==0) ? true : false; } //插入元素 template<class T> bool List<T>::Insert(int i,T x) { if(i<1 || i>length+1) return false; if(length >= listsize) return false; length++; for(int k=length-1;k>=i-1;--k) { data[k]=data[k-1]; } data[i-1]=x; } //刪除元素 template<class T> bool List<T>::Delete(int i) { if (length<=0) return false; if(i<1 || i>length) return false; // x = data[i-1]; for(int j=i-1; j<length-1; ++j) data[j] = data[j+1]; length -= 1; return true; } //列印順序表 template<class T> void List<T>::output() { for(int i=0;i<length;i++) std::cout<<data[i]<<" "; } int main() { List<int> L(10); L.Insert(1,2); L.Insert(2,5); L.Insert(1,10); L.Insert(1,3); L.Insert(1,10); L.Insert(1,9); L.Delete(2); cout<<"查詢元素3的位置為:"<<L.Locate(3)<<endl; L.output(); return 0; }
注意:連結串列中LinkList L與LinkList *L的區別
對於LinkList L: L是指向定義的node結構體的指標,可以用->運算子來訪問結構體成員,即L->elem,而(*L)就是個Node型的結構體了,可以用點運算子訪問該結構體成員,即(*L).elem;
對於LinkList *L:L是指向定義的Node結構體指標的指標,所以(*L)是指向Node結構體的指標,可以用->運算子來訪問結構體成員,即(*L)->elem,當然,(**L)就是Node型結構體了,所以可以用點運算子來訪問結構體成員,即(**L).elem;
在連結串列操作中,我們常常要用連結串列變數作物函式的引數,這時,用LinkList L還是LinkList *L就很值得考慮深究了,一個用不好,函式就會出現邏輯錯誤,其準則是:
如果函式會改變指標L的值,而你希望函式結束呼叫後儲存L的值,那你就要用LinkList *L,這樣,向函式傳遞的就是指標的地址,結束呼叫後,自然就可以去改變指標的值;
而如果函式只會修改指標所指向的內容,而不會更改指標的值,那麼用LinkList L就行了;
注:c語言中->和.的區別——->用於指標, .用於物件
"->"用於指向結構成員,它的左邊應為指向該結構型別的指標(結構指標),而"."的左邊應為該結構型別的變數(結構變數),如已定義了一個結構體struct student,裡面有一個int a;然後有一個結構體變數struct student stu及結構體變數指標struct student *p;且有p=&stu,那麼p->a和stu.a表示同一個意思。
在“結構”一單元中出現的->運算子成為“右箭頭選擇”,在使用中可以用p->a = 10;來代替(*p).a = 10;
二、線性表的鏈式儲存結構
有頭有尾:
頭指標的資料域不儲存任何資訊
頭指標:是指連結串列指向的第一個結點的指標,若連結串列有頭結點,則是指向頭結點的指標;頭指標具有標識作用,所以常用頭指標冠以連結串列的名字(指標變數的名字);無論連結串列為空,頭指標均不為空;頭指標是連結串列的必要元素。
頭結點是為了操作的統一和方便而設立的,放在第一個元素的結點之前,其資料域一般無意義(但可以用來存放連結串列的長度);有了頭結點,在對第一個元素結點前插入結點和刪除第一結點起操作與其他結點的操作就統一了;頭結點不一定是連結串列的必須要素。
單鏈表圖例
空連結串列圖例
結構指標描述單鏈表
typedef struct Node
{
ElemType data;
struct Node* Next;
}Node;
typedef struct Node* LinkList;
單鏈表的讀取GetElem:
•獲得連結串列第i個數據的演算法思路:
–宣告一個結點p指向連結串列第一個結點,初始化j從1開始;
–當j<i時,就遍歷連結串列,讓p的指標向後移動,不斷指向一下結點,j+1;
–若到連結串列末尾p為空,則說明第i個元素不存在;
–否則查詢成功,返回結點p的資料。
單鏈表的插入ListInsert:
•單鏈表第i個數據插入結點的演算法思路:
–宣告一結點p指向連結串列頭結點,初始化j從1開始;
–當j<1時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一結點,j累加1;
–若到連結串列末尾p為空,則說明第i個元素不存在;
–否則查詢成功,在系統中生成一個空結點s;
–將資料元素e賦值給s->data;
–單鏈表的插入剛才兩個標準語句;
–返回成功。
單鏈表的刪除ListDelete:
•單鏈表第i個數據刪除結點的演算法思路:
–宣告結點p指向連結串列第一個結點,初始化j=1;
–當j<1時,就遍歷連結串列,讓P的指標向後移動,不斷指向下一個結點,j累加1;
–若到連結串列末尾p為空,則說明第i個元素不存在;
–否則查詢成功,將欲刪除結點p->next賦值給q;
–單鏈表的刪除標準語句p->next = q->next;
–將q結點中的資料賦值給e,作為返回;
–釋放q結點。
#include <stdio.h>
#include <stdlib.h>
#include<time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
typedef int ElemType;
typedef struct Node
{
ElemType data;
struct Node *next;
}Node,*LinkList;
Status InitList(LinkList *L)
{
*L = (LinkList)malloc(sizeof(Node));
if(!(*L))
{
return ERROR;
}
(*L)->next = NULL;
return OK;
}
//讀取
//L為帶頭節點的單鏈表的頭指標
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;
p = L->next;
j = 1;
while(p && j<i )
{
p = p->next;
j++;
}
if(!p || j>i)
{
return ERROR;
}
*e = p->data;
return OK;
}
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
//此時連結串列中帶頭節點
j = 1;
while( p && j<i ) // 用於尋找第i個結點
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while( p->next && j<i )
{
p = p->next;
++j;
}
if( !(p->next) || j>i )
{
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); // 初始化隨機數種子
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for( i=0; i < n; i++ )
{
p = (LinkList)malloc(sizeof(Node)); // 生成新結點
p->data = rand()%100+1;
p->next = (*L)->next;
(*L)->next = p;
}
}
//尾插法
void CreateListTil(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));
//初始化隨機數種子
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for(i = 0; i<n ; i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;
}
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
void ShowList(LinkList L)
{
LinkList p;
p = L->next;
while(p)
{
printf("%d ",p->data);
p = p->next;
}
}
int main()
{
LinkList L;
Status i;
i = InitList(&L);
CreateListHead(&L,10);
ShowList(L);
return 0;
}
時間效能:
——查詢:順序儲存結構O(1)、單鏈表O(n)
——插入和刪除:順序儲存結構需要平均移動表長一半的元素,時間為O(n);單鏈表在計算出某位置的指標後,插入和刪除時間僅為O(1)
空間效能:
——順序儲存結構需要預分配儲存空間
——單鏈表不需要分配儲存空間,只要有就可以分配,元素個數不受限制。
綜上:若線性表需要頻繁查詢,很少進行插入和刪除操作時,可採用順序儲存結構。需要頻繁進行插入和刪除操作時,宜採用鏈式儲存結構。
思考:判斷單鏈表中是否有環——•有環的定義是,連結串列的尾節點指向了連結串列中的某個節點。
•方法一:使用p、q兩個指標,p總是向前走,但q每次都從頭開始走,對於每個節點,看p走的步數是否和q一樣。如圖,當p從6走到3時,用了6步,此時若q從head出發,則只需兩步就到3,因而步數不等,出現矛盾,存在環。
•方法二:(快慢指標)使用p、q兩個指標,p每次向前走一步,q每次向前走兩步,若在某個時候p == q,則存在環。
魔術師發牌問題:
•問題描述:魔術師利用一副牌中的13張黑牌,預先將他們排好後疊放在一起,牌面朝下。對觀眾說:“我不看牌,只數數就可以猜到每張牌是什麼,我大聲數數,你們聽,不信?現場演示。”魔術師將最上面的那張牌數為1,把他翻過來正好是黑桃A,將黑桃A放在桌子上,第二次數1,2,將第一張牌放在這些牌的下面,將第二張牌翻過來,正好是黑桃2,也將它放在桌子上這樣依次進行將13張牌全部翻出,準確無誤。
•問題:牌的開始順序是如何安排的?
#include<stdio.h>
#include<stdlib.h>
#define CardNumber 13
typedef struct Node
{
int data;
struct Node *next;
}Node,*LinkList;
LinkList CreateLinkList()
{
LinkList head = NULL;
LinkList s,r;
int i;
r = head;
for(i = 1; i<= CardNumber ; i++)
{
s = (LinkList)malloc(sizeof(Node));
s->data = 0;
if(head == NULL)
{
head = s;
}
else
{
r->next = s;
}
r = s;
}
r->next = head;
return head;
}
void Magician(LinkList head)
{
LinkList p;
int j;
int Countnumber = 2;
p = head;
p->data = 1;
while(1)
{
for(j=0 ; j<Countnumber ; j++)
{
p = p->next;
if(p->data !=0 )
{
p->next;
j--;
}
}
if(p->data ==0)
{
p->data = Countnumber;
Countnumber++;
if(Countnumber == 14)
break;
}
}
}
void DestoryList(LinkList *list)
{
LinkList ptr = *list;
LinkList buff[CardNumber];
int i =0;
while(i<CardNumber)
{
buff[i++] = ptr;
ptr = ptr->next;
}
for(i=0 ; i<CardNumber; ++i)
free(buff[i]);
*list = 0;
}
int main()
{
printf("按如下順序排列:\n");
LinkList p;
int i;
p = CreateLinkList();
Magician(p);
for(i=0 ; i<CardNumber ;i++)
{
printf("黑桃%d ",p->data);
p = p->next;
}
DestoryList(&p);
return 0;
}
三、靜態連結串列:用陣列代替指標描述單鏈表(遊標實現法)
下標為0或者是最後一個時,不存放資料。約定下標為最後一個的元素的遊標指向第一個有資料的下標,即1
約定下標為0的元素的遊標存放沒有存放資料的元素的下標,即5
最後一個存放資料的元素遊標為0
#define MAXSIZE 1000
typedef struct
{
ElemType data; //資料
int cur; //遊標
}Component, StaticLinkList[MAXSIZE];
靜態連結串列初始化相當於初始化陣列
Satus InitList(StaticLinkList space)
{
int i;
for(int i=0; i < MAXSIZE-1 ; i++)
{
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0;
}
return OK;
}
四、迴圈連結串列
迴圈連結串列和單鏈表的主要差異就在於迴圈的判斷空連結串列的條件上,原來判斷head->next是否為null,現在則是head->next是否等於head
當表尾的操作比較頻繁時,採用帶尾指標的迴圈連結串列顯然比較方便。
尾結點:*R
首結點:*(R->next->next)
五、雙向連結串列
結點結構
typedef struct DualNode
{
ElemType data;
struct DualNode *prior; //前驅結點
struct DualNode *next; //後繼結點
} DualNode, *DuLinkList;
雙向連結串列的插入刪除操作其實並不複雜,但是順序非常重要。
插入操作:
s->next = p;
s->prior = p->prior;
p->prior->next = s;
p->prior = s;
刪除操作
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
本章節鞏固練習
題目1:快速找到未知長度單鏈表的中間節點
解法:
——遍歷單鏈表確定連結串列長度L,然後再次從頭節點出發迴圈L/2次找到單鏈表的中間結點
演算法複雜度O(L+L/2)
——利用慢指標,原理:設定兩個指標*search、*mid都指向單鏈表的頭節點。其中*search的移動速度
是*mid的兩倍,當*search移動到末尾節點的時候,mid正好就在中間了。(標尺思想)
#include "stdio.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef int ElemType; /* ElemType型別根據實際情況而定,這裡假設為int */
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /* 定義LinkList */
//*LinkList 結構體指標型別
//表示用LinkList 代替 struct Node*
Status visit(ElemType c)
{
printf("%d ",c);
return OK;
}
/* 初始化順序線性表 */
Status InitList(LinkList *L)
{
*L=(LinkList)malloc(sizeof(Node)); /* 產生頭結點,並使L指向此頭結點 */
if(!(*L)) /* 儲存分配失敗 */
{
return ERROR;
}
(*L)->next=NULL; /* 指標域為空 */
return OK;
}
/* 初始條件:順序線性表L已存在。操作結果:返回L中資料元素個數 */
int ListLength(LinkList L)
{
int i=0;
LinkList p=L->next; /* p指向第一個結點 */
while(p)
{
i++;
p=p->next;
}
return i;
}
/* 初始條件:順序線性表L已存在 */
/* 操作結果:依次對L的每個資料元素輸出 */
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);
p = p->next;
}
printf("\n");
return OK;
}
/* 隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化隨機數種子 */
*L = (LinkList)malloc(sizeof(Node)); /* L為整個線性表 */
r=*L; /* r為指向尾部的結點 */
for (i=0; i < n; i++)
{
p = (Node *)malloc(sizeof(Node)); /* 生成新結點 */
p->data = rand()%100+1; /* 隨機生成100以內的數字 */
r->next=p; /* 將表尾終端結點的指標指向新結點 */
r = p; /* 將當前的新結點定義為表尾終端結點 */
}
r->next = NULL; /* 表示當前連結串列結束 */
// 建立有環連結串列
//r->next = p;
}
Status GetMidNode(LinkList L, ElemType *e)
{
LinkList search, mid;
mid = search = L;
while (search->next != NULL)
{
//search移動的速度是 mid 的2倍
if (search->next->next != NULL)
{
search = search->next->next;
mid = mid->next;
}
else
{
search = search->next;
}
}
*e = mid->data;
return OK;
}
int main()
{
LinkList L;
Status i;
char opp;
ElemType e;
int find;
int tmp;
i=InitList(&L);
printf("初始化L後:ListLength(L)=%d\n",ListLength(L));
printf("\n1.檢視連結串列 \n2.建立連結串列(尾插法) \n3.連結串列長度 \n4.中間結點值 \n0.退出 \n請選擇你的操作:\n");
while(opp != '0')
{
scanf("%c",&opp);
switch(opp)
{
case '1':
ListTraverse(L);
printf("\n");
break;
case '2':
CreateListTail(&L,20);
printf("整體建立L的元素(尾插法):\n");
ListTraverse(L);
printf("\n");
break;
case '3':
//clearList(pHead); //清空連結串列
printf("ListLength(L)=%d \n",ListLength(L));
printf("\n");
break;
case '4':
//GetNthNodeFromBack(L,find,&e);
GetMidNode(L, &e);
printf("連結串列中間結點的值為:%d\n", e);
//ListTraverse(L);
printf("\n");
break;
case '0':
exit(0);
}
}
}
題目2:寫一個完整的程式,實現隨機生成20個元素的連結串列(尾插法或頭插法任意),並用題1方法快速查詢中間結點的值並顯示。
題目3:用迴圈連結串列模擬約瑟夫問題,把41個人自殺的順序編號輸出。
(約瑟夫問題):
•據說著名猶太曆史學家 Josephus有過以下的故事:在羅馬人佔領喬塔帕特後,39個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡為止。
•然而Josephus和他的朋友並不想遵從,Josephus要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。
#include<stdio.h>
#include<stdlib.h>
typedef struct Node
{
int data;
struct Node *next;
}Node,*LinkList;
LinkList Create(int n)
{
LinkList p =NULL;
LinkList head;
head = (LinkList)malloc(sizeof(Node));
p = head;
LinkList s;
int i = 1;
if( 0 != n)
{
while(i <= n)
{
s = (LinkList)malloc(sizeof(Node));
s->data = i++;
p->next = s;
p = s;
}
s->next = head->next;
//指向第一個元素
}
free(head);
return s->next;
}
int main()
{
int n = 41;
int m = 3;
int i;
LinkList L = Create(n);
LinkList temp;
m %= n; // m在這裡是等於3
while (L != L->next )
{
for (i = 1; i < m-1; i++)
{
L = L->next ;
}
//將指標指向要找的數的前一個結點處
printf("%d->", L->next->data );
temp = L->next ; //刪除第m個節點
L->next = temp->next ;
free(temp);
L = L->next ;
}
printf("%d\n", L->data );
return 0;
}
題4(挑戰與提高):
•提高挑戰難度:編號為1~N的N個人按順時針方向圍坐一圈,每人持有一個密碼(正整數,可以自由輸入),開始人選一個正整數作為報數上限值M,從第一個人按順時針方向自1開始順序報數,報道M時停止報數。報M的人出列,將他的密碼作為新的M值,從他順時針方向上的下一個人開始從1報數,如此下去,直至所有人全部出列為止。
題5(雙向迴圈連結串列實踐)
–要求實現使用者輸入一個數使得26個字母的排列發生變化,例如使用者輸入3,輸出結果:
–DEFGHIJKLMNOPQRSTUVWXYZABC
–同時需要支援負數,例如使用者輸入-3,輸出結果:
–XYZABCDEFGHIJKLMNOPQRSTUVW
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define ERROR 0
typedef char ElemType;
typedef int Status;
typedef struct DualNode
{
ElemType data;
struct DualNode *prior;
struct DualNode *next;
}DualNode,*DuLinkList;
Status InitList(DuLinkList *L)
{
DualNode *p,*q;
int i;
*L = (DuLinkList)malloc(sizeof(DualNode));
if(!(*L))
{
return ERROR;
}
(*L)->next = (*L)->prior = NULL;
p = (*L);
for(i = 0 ; i<26 ; i++)
{
q = (DualNode *)malloc(sizeof(DualNode));
if(!q)
{
return ERROR;
}
q->data = 'A'+i;
q->prior = p;
q->next = p->next;
p->next = q;
p = q;
}
p->next = (*L)->next;
(*L)->next->prior = p;
return OK;
}
void caser(DuLinkList *L, int i)
{
if(i > 0)
{
do
{
(*L) = (*L)->next;
}while(--i);
}
if(i<0)
{
i = i-1;
(*L) = (*L)->next;
do
{
(*L) = (*L)->prior;
}while(++i);
}
}
int main()
{
DuLinkList L;
int i,n;
InitList(&L);
printf("請輸入一個整數:\n");
scanf("%d",&n);
printf("\n");
caser(&L,n);
for(i = 0; i<26 ; i++)
{
L= L->next;
printf("%c",L->data);
}
printf("\n");
return 0;
}