鏈表與數組
在編程語言中,數組數據結構(array data structure),簡稱數組(Array),是一種數據結構,是數據元素(elements)的集合。有限個相同類型的元素按順序存儲,用一個名字命名,然後用編號區分他們的變量的集合;這個名字稱爲數組名,編號稱爲下標。
鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,但是不同與數組的是,並不會按線性的順序存儲數據,而是在每一個節點裏存到下一個節點的指針(Pointer)。由於不必須按順序存儲。
一、鏈表與數組的優缺點
首先讓我們來介紹下鏈表與數組之間的優缺點:
// 數組的缺點:
//
// 1.一旦數組定義,則大小固定,無法進行修改(數組的大小)。
// 2.數組插入和刪除的效率太低,時間複雜度O(n)。
//
// 數組的優點:
//
// 1.下標訪問,速度快,時間複雜度是O(1)
//
//////////////////////////////////////////////////////////
//
// 鏈表:
//
// 鏈表的優點:
//
// 1.資源允許的情況下,規模可以不斷的增長或減小。
// 2.刪除和添加效率高,O(1)
//
// 鏈表的缺點:
//
// 鏈表的遍歷過程效率比較低。
爲什麼會有這樣的差異呢。
接下來我們一步步來看。
二、數據結構類型的定義
數組的定義:
//方法1
int array1[10] = {0};
//方法2
int array2[] = {12, 23, 34, 45, 56, 67, 78};
//方法3
int *array3 = NULL;
array3 = (int *)malloc(sizeof(int) * 100);
if(array3 != NULL){
fprintf(stderr, "the memory is full!\n");
exit(1);
}
這裏,我們的array1
和array2
都在定義時直接分配了棧上的內存空間;
而array3
是一個int類型的指針,指向我們在堆上用malloc
分配的4bytes ×100 = 400 bytes
大小的空間。
我們的數組雖然在O(1)的時間複雜度訪問下標進行數據存取查找,可是一般數組定義後,大小不能夠進行動態變化,且插入刪除效率過低,時間複雜度爲O(n).
所以,爲了彌補這些不足,我們又學習到了鏈表。
鏈表的定義
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,鏈表由相同結構的鏈表節點構成,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。可以關注鏈表節點的結構:
typedef struct List_node
{
int data; //數據域
struct List_node *next; //指針域
}List_node;
typedef List_node *List_head;
鏈表節點的兩個組成部分:
我們可以看到鏈表節點主要分爲兩大部分:數據域和鏈域。
數據域是我們最要存儲的內容,而鏈域則代表着一種指針。如何把兩個節點進行連接:
這個指針指向類型爲鏈表節點,這樣每一個節點就可以記錄下下一個節點的位置,從而把原本毫不相干的鏈表節點串接起來。
一個鏈表節點定義;
typedef struct Node{
//數據域
char name[20]; //姓名
char sex; //m 男 w 女 性別
char age; //年齡
char jobs[20]; //職業
//鏈域
struct Node *next;
}Node;
再申請一個鏈表節點;
Node node1 = {0};
Node *p_node = (Node *)malloc(sizeof(Node));
node1.next = p_node;
malloc 函數:
void *malloc(int size);
說明:malloc 向系統申請分配指定size個字節的內存空間。返回類型是 void* 類型。
man malloc
可在bash中查看幫助文檔。
三、鏈表的常見接口
我們把可以對鏈表的操作是在.h文件中進行聲明的,我們叫這樣的聲明爲接口,如下是單鏈表的接口聲明:
/* list.h */
/*
* 帶頭節點的鏈表
* */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#define TRUE (1)
#define FALSE (0)
typedef unsigned char Boolean;
//定義鏈表節點類型
typedef struct List_node{
int data; //數據域 //4
struct List_node *next; //指針域 //32位:4 //64位:8
}List_node;
typedef List_node *List_head;
//鏈表的接口
// 常見接口:
// 1.初始化
// 2.銷燬
// 3.增加
// 4.刪除
// 5.查找
// 6.修改
// 7.排序
// 8.顯示
List_head init_list(void) ; //1. 鏈表的初始化
void destroy_list(List_head *head) ; //2. 鏈表的銷燬
Boolean push_front(List_head head, int value) ; //3.1 頭部添加
Boolean push_back(List_head head, int value) ; //3.2 尾部添加(效率低,從頭找到尾才能添加)
Boolean pop_front(List_head head) ; //4.1 頭部刪除
Boolean pop_back(List_head head) ; //4.2 尾部刪除(效率低)
Boolean find_node(List_head head, int value, List_node **node) ; //5. 鏈表的查找
void modify_node(List_node *node, int value) ; //6.1 鏈表節點的修改
Boolean insert_node(List_head head, int index, int value) ; //6.2 鏈表節點的插入(下標)
void sort_list_ascend(List_head head) ; //7.1 鏈表升序
void sort_list_descend(List_head head) ; //7.2 鏈表降序
void show_list(List_head head) ; //8.1 顯示鏈表信息
int get_list_length(List_head head) ; //8.2 鏈表的長度
我們可以通過定義 包裹函數 來縮短程序。每個包裹函數完成實際的函數調用,檢查返回值,並在發生錯誤時終止進程。
包裹函數其實就是封裝函數,調用一個函數來實現這個功能。既然我們會經常用到一些函數並且還要進行錯誤處理,那麼,我們可以將這些函數封裝起來,也放入頭文件.h當中。
//包裹函數
static void *Malloc(size_t size);
static List_node *create_node(void);
static void swap(void *a, void *b, int length);
static void *Malloc(size_t size)
{
void *result = malloc(size);
if(result == NULL){ //失敗情況處理
fprintf(stderr, "the memory is full!\n");
exit(1);
}
return result;
}
static List_node *create_node(void)
{
//申請節點,並且對節點初始化(置爲0)
List_node *node = (List_node *)Malloc(sizeof(List_node));
bzero(node, sizeof(List_node));
return node;
}
static void swap(void *a, void *b, int length)
{
void *temp = Malloc(length);
memcpy(temp, a, length);
memcpy(a, b, length);
memcpy(b, temp, length);
free(temp);
}
四、鏈表接口的實現
爲了能夠使用這些我們所提供的接口,如同菜名需要菜譜一樣,我們要能夠實現這些操作,可以在list.c中進行實現。
//////////////////////////////////////////////////////////////////////////////////////
//接口實現
//////////////////////////////////////////////////////////////////////////////////////
List_head init_list(void) //1. 鏈表的初始化
{
List_head head = NULL;
head = create_node();
return head;
}
void destroy_list(List_head *head) //2. 鏈表的銷燬
{
List_node *p = NULL;
List_node *q = NULL;
if(head == NULL || *head == NULL){
return ;
}
//從鏈表頭節點開始,依次對節點進行刪除
p = *head;
while(p != NULL){
q = p;
p = p->next;
free(q);
}
*head = NULL;
}
Boolean push_front(List_head head, int value) //3.1 頭部添加
{
List_node *node = NULL;
if(head == NULL){ //判斷鏈表是否存在
return FALSE;
}
//生成鏈表節點並且賦初值
node = create_node();
node->data = value;
node->next = head->next;
head->next = node;
head->data++;
return TRUE;
}
Boolean push_back(List_head head, int value) //3.2 尾部添加(效率低,從頭找到尾才能添加)
{
List_node *p_node = NULL;
if(head == NULL){ //參數檢測
return FALSE;
}
//查找尾部節點的位置
p_node = head;
while(p_node->next != NULL){
p_node = p_node->next;
}
//把新生成的節點追加到鏈表末尾
p_node->next = create_node();
p_node->next->data = value;
head->data++;
return TRUE;
}
Boolean pop_front(List_head head) //4.1 頭部刪除
{
List_node *p_node = NULL;
if(head == NULL || head->next == NULL){
//鏈表不存在或者沒有有效節點
return FALSE;
}
p_node = head->next;
head->next = p_node->next;
free(p_node);
head->data--;
return TRUE;
}
Boolean pop_back(List_head head) //4.2 尾部刪除(效率低)
{
List_node *p_node = NULL;
//參數檢測
if(head == NULL || head->next == NULL){
return FALSE;
}
//從頭結點開始查找倒數第二個節點
p_node = head;
while(p_node->next->next != NULL){
p_node = p_node->next;
}
//刪除最後一個節點,把倒數第二個置爲最後一個節點
free(p_node->next);
p_node->next = NULL;
head->data--;
return TRUE;
}
Boolean find_node(List_head head, int value, List_node **node) //5. 鏈表的查找
{
List_node *p_node = NULL;
if(head == NULL){
return FALSE;
}
//從第一個有效節點開始查找值爲value的節點
p_node = head->next;
while(p_node != NULL){
if(p_node->data == value){ //找到value所在節點
if(node != NULL){
*node = p_node;
}
return TRUE;
}
p_node = p_node->next;
}
return FALSE;
}
void modify_node(List_node *node, int value) //6.1 鏈表節點的修改
{
node->data = value;
}
Boolean insert_node(List_head head, int index, int value) //6.2 鏈表節點的插入
{
List_node *node = NULL;
List_node *p_node = NULL;
int count = index;
if(head == NULL || index < 0 || index > head->data){
//鏈表不存在並且下標不符合規則
return FALSE;
}
//創建新的節點
node = create_node();
node->data = value;
//尋找插入的位置
p_node = head;
while(count--){ //找到被插入節點的前一個節點
p_node = p_node->next;
}
//插入新的節點
node->next = p_node->next;
p_node->next = node;
head->data++;
return TRUE;
}
void sort_list_ascend(List_head head) //7.1 鏈表升序
{
List_node *p_node = NULL;
List_node *q_node = NULL;
if(head == NULL || head->data < 2){
return ;
}
for(p_node = head->next; p_node->next ; p_node = p_node->next){
for(q_node = p_node->next; q_node; q_node = q_node->next){
if(p_node->data > q_node->data){
swap(p_node, q_node, (unsigned long)(&((List_node *)0)->next));
}
}
}
}
void sort_list_descend(List_head head) //7.2 鏈表降序
{
List_node *p_node = NULL;
List_node *q_node = NULL;
if(head == NULL || head->data < 2){
return ;
}
for(p_node = head->next; p_node->next ; p_node = p_node->next){
for(q_node = p_node->next; q_node; q_node = q_node->next){
if(p_node->data < q_node->data){
swap(p_node, q_node, sizeof(List_node) - sizeof(List_node *));
}
}
}
}
void show_list(List_head head) //8.1 顯示鏈表信息
{
List_node *p_node = NULL;
if(head == NULL){
return ;
}
for(p_node = head->next; p_node != NULL; p_node = p_node->next){
printf("%d ", p_node->data);
}
printf("\n");
}
int get_list_length(List_head head) //8.2 鏈表的長度
{
if(head == NULL){
return -1;
}
return head->data;
}
設計完這個單鏈表的接口與實現之後,我們要對相應功能進行檢測。
測試代碼如下:
//主函數-各個函數的測試
int main(int argc, char ** argv)
{
int i = 0;
List_head head = NULL;
head = init_list();
List_node *node = create_node();
printf("\n頭部插入:\n");
for(i = 0 ; i < seed ; i++ )
{
push_front(head,rand()%100);
}
show_list(head);
printf("\n尾部插入:\n");
for(i = 0 ; i < seed ; i++ )
{
push_back(head,rand()%100);
}
show_list(head);
printf("\n頭部刪除:\n");
pop_front(head);
show_list(head);
printf("\n尾部刪除:\n");
pop_back(head);
show_list(head);
printf("\n插入節點:\n");
insert_node(head, 3, 100);
show_list(head);
find_node(head, 77,&node);
printf("\n升序排序:\n");
sort_list_ascend(head);
show_list(head);
printf("\n降序排序:\n");
sort_list_descend(head);
show_list(head);
printf("\n修改節點數據:\n");
node = head->next->next;
modify_node(node, 777);
show_list(head);
printf("\nthe length of this list :%d\n",get_list_length(head));
destroy_list(&head);
return 0;
}
運行結果:
root@aemonair# ./my_head_list
鏈表初始化成功!
頭部插入:
當前鏈表如下:
93 15 77 86 83
尾部插入:
當前鏈表如下:
93 15 77 86 83 35 86 92 49 21
頭部刪除:
當前鏈表如下:
15 77 86 83 35 86 92 49 21
尾部刪除:
當前鏈表如下:
15 77 86 83 35 86 92 49
插入節點:
當前鏈表如下:
15 77 86 100 83 35 86 92 49
the node is found !
升序排序:
當前鏈表如下:
15 35 49 77 83 86 86 92 100
降序排序:
當前鏈表如下:
100 92 86 86 83 77 49 35 15
修改節點數據:
當前鏈表如下:
100 777 86 86 83 77 49 35 15
the length of this list :9
鏈表銷燬完畢!
測試程序2:
int main(int argc, char **argv)
{
List_head head = init_list(); //鏈表的初始化
int i = 0;
int value = 0;
List_node *find = NULL;
for(i = 0; i < 10; ++i){ //尾部追加10個元素
push_front(head, rand() % 100);
}
pop_front(head);
pop_back(head);
show_list(head); //顯示鏈表信息
printf("the count of list:%d\n", get_list_length(head));
printf("你要找哪個節點:?\n");
scanf("%d", &value);
find_node(head, value, &find);
if(find == NULL){
printf("the %d is not found!\n", value);
}else{
printf("found! the value is %d\n", find->data);
}
sort_list_ascend(head);
show_list(head); //顯示鏈表信息
sort_list_descend(head);
show_list(head); //顯示鏈表信息
destroy_list(&head);
return 0;
}
總結:
以上,我們關於單鏈表的基本操作和基本接口的實現。
對於鏈表的指針域在交換兩個的時候很容易出現問題,之後還會進行進一步的詳解。