數據結構與算法(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)

空間性能:

——順序存儲結構需要預分配存儲空間

——單鏈表不需要分配存儲空間,只要有就可以分配,元素個數不受限制。

綜上:若線性表需要頻繁查找,很少進行插入和刪除操作時,可採用順序存儲結構。需要頻繁進行插入和刪除操作時,宜採用鏈式存儲結構。

 

思考:判斷單鏈表中是否有環——•有環的定義是,鏈表的尾節點指向了鏈表中的某個節點。

 

方法一:使用pq兩個指針,p總是向前走,但q每次都從頭開始走,對於每個節點,看p走的步數是否和q一樣。如圖,當p6走到3時,用了6步,此時若qhead出發,則只需兩步就到3,因而步數不等,出現矛盾,存在環。

方法二:(快慢指針)使用pq兩個指針,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;
}

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章