資料結構-邏輯關係&物理關係、時間複雜度、空間複雜度、順序表

逸風明發表於2024-11-05

一、資料結構概述

基本概念

資料結構指的是計算機儲存資料和組織資料的方式,儲存資料和組織資料的目的是為了後期對資料的再次利用,所以儲存的資料一般是具有一個或者多個特定關係的集合,利用不同的資料結構可以提高資料的訪問效率。

思考:為什麼大家來到新教室選好座位之後需要填寫座位表?? 答案:方便管理班級學生

資料指的是可以被輸入到計算機並且可以被計算機處理的符號的總稱,資料的英文是Data。


資料結構

資料是有單位的,資料的基本單位是資料元素(Data Element),在計算機中資料元素是作為整體來處理的,比如學生的資訊。資料元素是由多個資料項組成的,所以資料項也被稱為資料的最小單位,比如學生資訊中的學號、姓名、年齡,資料項屬於資料元素不可分割的一部分。

舉例:比如國家是資料元素,則每個國家的城市就是資料項,資料項是資料不可分割的部分。
image

注意:世界上不止有一個國家,如果每個國家都是資料元素的話,則多個資料元素的集合就被稱為資料物件(Data Object)。
image

資料結構就是描述多個資料之間的邏輯結構和物理結構。邏輯結構指的是資料元素之間的邏輯關係,物理結構指的是計算機中儲存資料的方式,所以物理結構也被稱為儲存結構。

注意:資料元素的邏輯關係和物理關係沒有必然的聯絡,資料元素可能同時儲存邏輯關係和物理關係,資料元素之間也可能只存在一種關係,或者資料元素之間一種關係都沒有。

邏輯關係

對於資料結構的邏輯關係,可以分為四種:集合(無關係)、線性結構(一對一)、樹狀結構(一對多)、圖狀結構(多對多)。
image

物理關係

資料的物理關係可以分為兩種:一種是順序結構(連續儲存),另一種是離散結構(離散儲存),一般把順序結構也稱為順序儲存,一般把離散結構也稱為鏈式儲存,兩種區別如下圖
image
image

image

演算法概念

廣義上講演算法是研究資料之間的邏輯關係,然後選擇某種方案來儲存資料,並在此基礎上對資料進行處理,其實更加直白的說:演算法指的是計算或者解決問題的步驟。

請問:如果把下面的一個隨機數列中的數值按照從小到大順序進行排列??具體步驟是什麼??
image

演算法特徵

(1)有窮性:指的是程式執行必須在有限次數內完成,而每一次必須在有限時間內執行完成。

(2)確定性:執行的每一條語句都必須有準確的解釋,不能出現二義性,意味著相同的輸入 就會相同的輸出。

(3)可行性:程式中每一條複雜語句都可以分解為基本指令,並且每條基本指令都必須在有 限時間完成。
(4)輸入項:指的是演算法可以有一個或者多個引數作為初始條件,然後對程式進行有效執行。

(5)輸出項:指的是演算法經過運算之後可以有一個或者多個輸出,所以一個有意義的演算法是 應該有輸出結果的。

總結:一個程式的執行是需要使用者選擇合適的演算法和資料結構的,程式 = 資料結構+演算法。

思考:到底什麼樣的資料結構和演算法是合適的?怎麼去評定選擇的資料結構和演算法是否合適?

回答;對於資料結構的選擇和演算法的選擇並不是唯一的,但是選擇要是合適的,衡量資料結構和演算法的選擇是否合適,取決於演算法實現的執行時間和記憶體空間。一般是透過兩個專業性名稱,分別是“時間複雜度”和“空間複雜度”。

時間複雜度

時間複雜度不是演算法的執行時間來衡量,因為程式的執行時間取決於CPU的效能,不同效能的CPU執行指令的週期是不一樣的,比如8bit微控制器的主頻是12MHZ,而32bit微控制器的主機可以168MHZ,而計算機的CPU主頻都是xxx.GHZ 。

時間複雜度指的是演算法程式的語句的執行次數,也可以稱為語句頻度,一個程式的語句執行次數越多,則時間複雜度越大,則說明演算法不合適。時間複雜度一般採用數學符號大O()表示,一般時間複雜度的計算中都會出現n,n表示規模,對於時間複雜度是表示演算法的趨勢。

一般會把演算法程式的語句的執行次數用T()表示,但是對於函式T()可能是一個多項式,而時間複雜度就是找出函式T()影響最大的項,所以時間複雜度是執行語句的估算值,使用數學符號大O()表示。O其實是order的縮寫。大O的括號中寫的值就是影響程式執行語句最大的那個項。
image
image

計算技巧:只需要計算出演算法的基本執行語句的最高次項,並且把最高次項的係數捨棄,就是演算法的時間複雜度,需要使用數學符號O(xxx),如果計算出的是常數項,則時間複雜度衡為O(1)。
image

空間複雜度

空間複雜度指的是程式執行期間所需要的記憶體空間,空間複雜度越大,則說明程式執行期間需要的記憶體越多,則說明演算法不合適。

注意:程式中的時間複雜度和空間複雜度是可以互相轉換的,一般情況下是相互制約的,意味著“魚和熊掌不可兼得”,所以使用者根據實際情況去選擇時間還是空間,意味著要選擇合適的演算法來保持平衡。

一個好的演算法通常是執行時間短,佔用空間少,並且可讀性好、容易維護,易於移植到其他平臺。
image

結構型別

image

大家在學習C語言的時候接觸的陣列在資料結構中是屬於線性表的一種,線性表是由一組具有n個相同型別的資料元素組成的。

線性表中的任何一個資料元素有且只有一個直接前驅,以及有且只有一個直接後繼,另外首元素是沒有前驅的,尾元素是沒有後繼的。
image

某個元素的左側相鄰元素被稱為“直接前驅”,元素左側所有的資料元素被稱為“前驅元素”。
某個元素的右側相鄰元素被稱為“直接後繼”,元素右側所有的資料元素被稱為“後繼元素”。

image

滿足這種數學關係的一組元素,邏輯關係就是線性結構,並且邏輯關係是一對一的,比如一個教室學生的學號、一個排隊的隊伍、一摞堆好的盤子.....都屬於線性結構,當然線性結構和儲存方式是無關的,簡單理解:只有邏輯關係是一對一的,就是線性結構。

所以,根據資料的儲存方式可以把線性表分為兩種:順序儲存的線性表,鏈式儲存的線性表。

順序表

順序表指的是使用一組記憶體地址連續的記憶體單元來依次儲存線性表中的資料元素,使用這種儲存結構的線性表就被稱為順序表。

簡單理解:資料儲存在一塊連續的記憶體中,在C語言中可以具名的陣列,也可以使用匿名的陣列(堆記憶體)。

順序表的特點:資料元素之間的邏輯關係是相鄰的,並且記憶體地址也是相鄰的,所以只要知道儲存線性表的第一個資料元素的記憶體地址,就可以對線性表中的任意一個元素進行隨機訪問。通常使用者使用動態分配的陣列來實現順序表,也就是使用堆記憶體實現。
image

隨機訪問指的是在同等時間內具有訪問任意元素的能力,和隨機訪問相對立的就是順序訪問,順序訪問花費的時間要高於隨機訪問,比如卷軸(順序)和書籍(隨機)、磁帶(順序)和唱片(隨機)。

練習:請問該筆試題的結果是什麼?請給出簡單的推理過程,請獨立完成該筆試題的分析。
image

練習:請問該筆試題的結果是什麼?請給出簡單的推理過程,請獨立完成該筆試題的分析。
image

練習:請問該筆試題的結果是什麼?請給出簡單的推理過程,請獨立完成該筆試題的分析。
image

思考:既然陣列可以作為線性表來使用,請問如何對陣列中的元素進行增加和刪除以及訪問?

回答:如果打算使用陣列實現線性表的特性,需要知道三個條件:陣列首元素地址、陣列元素的容量、陣列有效的最後一個元素的下標。
image

筆試題:
image
image

筆試題:
image
image


程式碼

sequencelist.c程式碼
/********************************************************************************************************
*
*
* 該程式實現順序表元素的增刪改查,目的是提高設計程式的邏輯思維,另外為了提高可移植性,所以順序表中元素的
* 資料型別為DataType_t,使用者可以根據實際情況修改順序表中元素的型別。
*
* 另外,為了方便管理順序表,所以使用者設計SeqList_t結構體,該結構體中包含三個成員:地址+容量+有效元素的下標
*
* 
*
* Copyright (c)  2023-2024   yfm3262@163.com   All right Reserved
* ******************************************************************************************************/
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>




//指的是順序表中的元素的資料型別,使用者可以根據需要進行修改
typedef int  DataType_t;

//構造記錄順序表SequenceList各項引數(順序表的首地址 + 順序表的容量 + 順序表中最後有效元素的下標)的結構體
typedef struct SequenceList
{
	DataType_t * Addr;		//記錄順序表首地址
	unsigned int Size;		//記錄順序表的容量
	int			 Last;      //順序表中最後元素的下標	

}SeqList_t;


//建立順序表並對順序表進行初始化
SeqList_t * SeqList_Create(unsigned int size)
{
	//1.利用calloc為順序表的管理結構體申請一塊堆記憶體
	SeqList_t *Manager = (SeqList_t *)calloc(1,sizeof(Manager));

	if(NULL == Manager)
	{
		perror("calloc memory for manager is failed");
		exit(-1); //程式異常終止
	}

	//2.利用calloc為所有元素申請堆記憶體
	Manager->Addr = (DataType_t *)calloc(size,sizeof(DataType_t));

	if (NULL == Manager->Addr)
	{
		perror("calloc memory for element is failed");
		free(Manager);
		exit(-1); //程式異常終止
	}

	//3.對管理順序表的結構體進行初始化(元素容量 + 最後元素下標)
	Manager->Size = size;	//對順序表中的容量進行初始化
	Manager->Last = -1;		//由於順序表為空,則最後元素下標初值為-1
	
	return Manager;
}


//判斷順序表是否已滿
bool SeqList_IsFull(SeqList_t *Manager)
{
	return (Manager->Last + 1 == Manager->Size) ? true : false;
}


//向順序表的尾部加入元素
bool SeqList_TailAdd(SeqList_t *Manager, DataType_t Data)
{
	//1.判斷順序表是否已滿
	if ( SeqList_IsFull(Manager) )
	{
		printf("SequenceList is Full!\n");
		return false;
	}

	//2.如果順序表有空閒空間,則把新元素新增到順序表尾部
	Manager->Addr[++Manager->Last] = Data;

	return true;
}

//向順序表的頭部加入元素
bool SeqList_HeadAdd(SeqList_t *Manager, DataType_t Data)
{
	//1.判斷順序表是否已滿
	if ( SeqList_IsFull(Manager) )
	{
		printf("SequenceList is Full!\n");
		return false;
	}

	//2.如果順序表有空閒空間,則需要把順序表所有元素向後移動1個單位
	for (int i = Manager->Last;i >= 0;i--)
	{
		Manager->Addr[i+1] = Manager->Addr[i];
	}

	//3把新元素新增到順序表的頭部,並且更新管理結構體中的元素下標+1
	Manager->Addr[0] = Data;
	Manager->Last++;

	return true;
}



//判斷順序表是否已滿
bool SeqList_IsEmpty(SeqList_t *Manager)
{
	return (-1 == Manager->Last) ? true : false;
}



//刪除順序表的元素
bool SeqList_Del(SeqList_t *Manager,DataType_t DestVal)
{
	int temp = -1;  //記錄要刪除的元素的下標

	//1.判斷順序表是否為空
	if ( SeqList_IsEmpty(Manager) )
	{
		printf("SequenceList is Empty!\n");
		return false;
	}

	//2.此時需要查詢目標值是否在順序表中
	for (int i = 0; i <= Manager->Last; ++i)
	{	
		//如果目標值和順序表中元素的值相同
		if (DestVal == Manager->Addr[i])
		{
			temp = i; //把目標元素的下標備份到變數temp中
			break;
		}		
	}
	
	//3.如果順序表沒有目標值的元素則直接終止函式
	if (-1 == temp)
	{
		printf("destval [%d] is not found\n",DestVal);
		return false;
	}

	//4.如果找到了目標元素,則直接把該元素的後繼元素向前移動一個單位
	for (int i = temp ; i < Manager->Last ; ++i)
	{
		Manager->Addr[i] = Manager->Addr[i+1];
	}

	//5.由於刪除了一個元素,則需要讓順序表的有效元素下標-1
	Manager->Last--;

	return true;
}


//遍歷順序表的元素
void SeqList_Print(SeqList_t *Manager)
{
	for (int i = 0; i <= Manager->Last; ++i)
	{
		printf("Element[%d] = %d\n",i,Manager->Addr[i]);
	}
}


int SeqList_Remove(*L,int p)
{
	//判斷順序表的地址是否有效
	if(NULL == L)
	{
		return 0;
	}

	int e = 0; //變數e,記錄待刪除元素的值


	//把待刪除元素的值備份到變數e中
	e = L[p];

	//把待刪除元素的後繼元素向前移動一個單位
	for (int i = p; i < length; ++i)
	{
		L[i] = L[i+1];
	}

	return 1;
}


//遞增排序  1 2 30 40  55     
void  SeqList_Insert(SeqList *L,int x)
{
	int temp = -1; //記錄待插入元素的下標

	//遍歷順序表,找到插入位置,比較元素
	for (int i = 0; i <= last; ++i)
	{
		if (x < L[i])
		{
			temp = i;
			break;
		}
	}

	if( -1 == temp)
	{
		L[last+1] = x;
		return;
	}

	//把待插入位置的後繼元素向後移動
	for (int i = last; i >= temp; i--)
	{
		L[i+1] = L[i];
	}

	L[temp] = x;
}



int main(int argc, char const *argv[])
{

	//1.建立順序表
	SeqList_t * Manager = SeqList_Create(10);
	
	//2.向順序表中的尾部插入新元素
	SeqList_TailAdd(Manager,5);
	SeqList_TailAdd(Manager,2);
	SeqList_TailAdd(Manager,1);
	SeqList_TailAdd(Manager,4);
	SeqList_TailAdd(Manager,6);  


	//3.遍歷順序表
	SeqList_Print(Manager); // -- 5 2 1 4 6
	printf("\n");
	//4.向順序表中的頭部插入新元素
	SeqList_HeadAdd(Manager,9);
	SeqList_HeadAdd(Manager,7);
	SeqList_HeadAdd(Manager,8);
	SeqList_HeadAdd(Manager,0);
	SeqList_HeadAdd(Manager,10);  

	//5.遍歷順序表
	SeqList_Print(Manager); // --10 0 8 7 9 5 2 1 4 6
	printf("\n");	
	//6.刪除順序表的元素
	SeqList_Del(Manager,20);
	SeqList_Del(Manager,5);
	SeqList_Del(Manager,10);
	SeqList_Del(Manager,0);
	SeqList_Del(Manager,30);

	//7.遍歷順序表
	SeqList_Print(Manager); // --8 7 9 2 1 4 6
	printf("\n");
	return 0;
}


相關文章