帶頭結點的線性連結串列的基本操作
持續了好久,終於有了這篇部落格,連結串列的操作需要藉助影象模型進行反覆學習,這裡儘可能的整理並記錄下自己的思考,以備後面複習,和大家分享。需要說明的是,我們從實際應用角度出發重新定義了線性表。
一. 定義
從上一篇文章可以看到,由於連結串列在空間的合理利用上和插入、刪除時不需要移動等優點,因此在很多場合下,它是線性表的首選儲存結構。然而,它也存在某些實現的缺點,如求線性表的長度時不如順序儲存結構的缺點。因此,從實際應用角度出發重新定義了線性表。
一個帶頭結點的線性連結串列型別定義如下:
//結點型別
typedef struct Node
{
ElemType data;
struct Node *next;
}Node,*PNode;
//連結串列型別
typedef struct List
{
PNode head; //指向線性連結串列的頭結點
PNode tail; //指向線性連結串列的尾結點
size_t size; //線性表中資料元素的個數
}List;
二、函式實現的難點
我們基於一個改良的連結串列型別,進行常見的基本操作,這裡將就每個函式實現的重難點找出來,使其核心精髓展現在我們眼前:
InitList(List *list)
該函式實現了將我們定義的連結串列進行初始化,使其頭指標、尾指標都指向頭結點(空表),連結串列的大小size為0.
void push_back(List *list,ElemType x)
尾插入,首先構建一個節點,然後將其插入連結串列尾部。此時體現出來該種資料結構的優勢,我們無需從連結串列首部直接操作,直接找到了連結串列尾部對其操作。
void push_front(List *list,ElemType x)
頭插,核心是用於在頭結點和第一個結點之間插入新的子節點。當然,如果插入前為空表,需修改連結串列的尾結點指向第一個結點。(因為是對頭部操作,我們需要留意尾結點)
void pop_back(List *list)
尾部刪除,首先判斷連結串列是否非空,然後不停的刪除尾結點。當然,需要找到尾結點的前驅,我們需要順序遍歷。
void pop_front(List *list)
頭部刪除,首先判斷連結串列是否非空,然後不停的刪除頭結點第一個結點。需要注意,如果刪除到只剩頭結點時,需要將尾指標指向頭結點。(因為是對頭部操作,我們需要留意尾結點)
void insert_val(List *list, ElemType x)
插入值,也即插入一個結點。前提是有序,按值插入(需考慮插入到尾結點之後情況:此時更新尾結點指標)。
Node* find (List *list, ElemType key)
void delete_val(List *list,ElemType key)
刪除值,呼叫了find方法,我們首先找到指向當前要刪除值的結點,然後將下一結點的值賦值給當前結點,使當前結點指向下一結點的next,刪除下一結點,從而變相的刪除了當前結點。
當然,我們也可以通過順序遍歷,找到當前要刪除值結點的前驅,然後刪除該節點。
void sort(List *list)
本篇文章的重點:排序。之前的實現中,我們都是通過某種排序方法,交換結點中的data,從而實現。本篇文章我們通過交換結點的方法來實現。
首先,我們把整個單鏈表斷開,已排序部分僅僅包括頭結點和第一個結點(尾指標指向第一個結點),待排序部分從第二個結點開始餘下的部分。
我們每次從待排序部分取出一個結點s,將其值和已排序部分順序比較,從而找到要插入位置的前驅p,插入前要更新q為待排序連結串列的第一個結點。然後尾插即可。
void reverse(List *list)
本篇文章的重點:反轉。同上,我們還是把整個單鏈表斷開分為兩部分:已排序部分僅僅包括頭結點和第一個結點(尾指標指向第一個結點),待排序部分從第二個結點開始餘下的部分。
我們每次從待排序部分取出一個結點給p,然後在頭結點和第一個結點之間頭插實現。
三、程式碼
sList.h
#ifndef SLIST_H__
#define SLIST_H__
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
typedef int ElemType;
//結點型別
typedef struct Node
{
ElemType data;
struct Node *next;
}Node,*PNode;
//連結串列型別
typedef struct List
{
PNode head;
PNode tail;
size_t size;
}List;
void InitList(List *list);
void push_back(List *list,ElemType x); //尾插
void push_front(List *list,ElemType x); //頭插
void pop_back(List *list); //尾刪
void pop_front(List *list); //頭刪
void insert_val(List *list, ElemType x); //按值插入
Node* find (List *list, ElemType key);//查詢某值
int length(List *list); //長度
void delete_val(List *list,ElemType key); //刪除值
void sort(List *list);//升序
void reverse(List *list);//反轉
void clear(List *list);//清除
void destroy(List *list);//銷燬
void show_list(List *list);
#endif // SLIST_H__
sList.cpp
#include "sList.h"
void InitList(List *list)
{
list->head = list->tail = (Node *)malloc(sizeof(Node));
assert(list->head != NULL);
list->head->next = NULL; //頭結點下一個為空
list->size = 0;
}
//1.更新新尾結點(一直對尾結點操作,故不需考慮)
void push_back(List *list,ElemType x)
{
Node *s = (Node *)malloc(sizeof(Node));
assert (s != NULL);
s->data = x;
s->next = list->tail->next;
list->tail->next = s; //原尾結點指向s
list->tail = s;//更新為新尾結點
//由於一直對尾指標考慮,因此這裡不需要考慮size=0
list->size++;
}
//1.在頭結點之後插入作為第一個結點(需要考慮尾結點)
void push_front(List *list,ElemType x)
{
Node *s = (Node *)malloc(sizeof(Node));
assert (s != NULL);
s->data = x;
s->next = list->head->next; //s結點指向第一個結點
list->head->next = s; //頭結點指向第一個結點
//插入時,如果是第一個結點(size=0)則需修改list->tail指標指向s
if(list->size == 0)
{
list->tail = s;
}
list->size++;
}
//1.判斷非空 2.使倒數第二個結點為新尾結點(一直對尾結點操作,故不需考慮)
void pop_back(List *list)
{
if(list->size == 0)
{
printf("error: seqList is empty\n");
return;
}
Node *p = list->head;
while (p->next != list->tail) { p = p->next; }
free(list->tail);//刪除舊尾結點
list->tail = p;//新尾結點
list->tail->next = NULL;//新尾結點next為NULL
list->size--;
}
//1.判斷非空 2.刪除頭結點之後第一個結點 3.需要考慮尾結點
void pop_front(List *list)
{
if(list->size == 0)
{
printf("error: seqList is empty\n");
return;
}
Node *q = list->head->next;
list->head->next = q->next;
free(q);
//刪除時,如果只剩第一個結點(size=1)則需修改list->tail指標指向頭結點
if(list->size == 1)
{
list->tail = list->head;
}
list->size--;
}
//前提:有序。按值插入(考慮插入到尾結點之後情況)
void insert_val(List *list, ElemType x)
{
Node *s = (Node *)malloc(sizeof(Node));
assert(s != NULL);
s->data = x;
s->next = NULL;
Node *p = list->head;
while(p->next != NULL && p->next->data <x)
p=p->next;
//若插入到尾結點,則更新尾結點
if(p->next == NULL)
{
list->tail = s;
}
//找到該插入位置的前一位
s->next = p->next;
p->next = s; //指向新建立的結點
list->size++;
}
Node* find (List *list, ElemType key)
{
Node *p = list->head->next;
//利用短路條件,注意順序
while( p!=NULL && p->data != key) { p = p->next; }
return p;
}
int length(List *list)
{
return list->size;
}
void delete_val(List *list,ElemType key)
{
if(list->size == 0)
return;
Node *p = find(list,key);
if(p == NULL)
{
printf("delete val not exist\n");
return ;
}
if(p == list->tail)
{
pop_back(list);
}else
{
//將要刪除資料的next拿過來替換值,取出next的有用資訊後free掉q
Node *q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
list->size--;
}
}
//把整個單鏈表斷開,把剩下連結串列的結點根據值升序尾插
void sort(List *list)
{
if(list->size==0 || list->size == 1)
return ;
Node *s = list->head->next; //指向第一個結點
Node *q = s->next;//指向第二個結點
list->tail = s;
list->tail->next = NULL; //斷開連結串列
//按值插入
while(q!=NULL)
{
s = q;
q = q->next;
//p指標為已排序指標,s為待排序指標
Node *p = list->head;
while(p->next != NULL && p->next->data < s->data)
p=p->next;
//若插入到尾結點,則更新尾結點
if(p->next == NULL)
{
list->tail = s;
}
//尾插
s->next = p->next;
p->next = s; //指向新建立的結點
}
}
//把整個單鏈表斷開,把剩下連結串列的結點按值頭插
void reverse(List *list)
{
if(list->size==0 || list->size == 1)
return ;
Node *p = list->head; //指向第一個結點
Node *q = p->next;//指向第二個結點
list->tail = p;//指向第一個結點
list->tail->next = NULL; //斷開連結串列
while(q != NULL)
{
p = q;
q = q->next;
p->next = list->head->next ;
list->head->next = p; //在頭結點和第一個結點直接插入
}
}
void clear(List *list)
{
if(list->size ==0)
return ;
Node *p = list->head->next;
while(p!=NULL)
{
list->head->next = p->next;
free(p);
p = list->head->next;
}
list->tail = list->head;
list->size = 0;
}
void destroy(List *list)
{
clear(list);
free(list->head);
list->tail = list->head =NULL;
}
void show_list(List *list)
{
Node *p = list->head->next;
while(p)
{
printf("%d--->",p->data);
p = p->next;
}
printf("Nul\n");
}
main.cpp
#include "sList.h"
/*
break語句通常用在迴圈語句和開關語句中。
當break用於開關語句switch中時,可使程式跳出switch而執行switch以後的語句;如果沒有break語句,則會從滿足條件的地方(即與switch(表示式)括號中表達式匹配的case)開始執行,直到switch結構結束。
當break語句用於do-while、for、while迴圈語句中時,可使程式終止迴圈。而執行迴圈後面的語句,通常break語句總是與if語句聯在一起。即滿足條件時便跳出迴圈。
*/
int main()
{
List mylist;
InitList(&mylist);
int select;
ElemType num;
Node *p;
while(select)
{
printf("* [1] push_back [2] push_front *\n");
printf("* [3] show_list [4] pop_back *\n");
printf("* [5] pop_front [6] insert_val *\n");
printf("* [7] find [8] length *\n");
printf("* [9] delete_val [10] sort *\n");
printf("* [11] reverse [12] clear *\n");
printf("* [13] destroy [0] quit_system *\n");
printf("please choose:>");
scanf("%d",&select);
if(select == 0) break;
switch(select)
{
case 1:
printf("push_back: input numbers(-1 erminate):");
while(scanf("%d",&num),num != -1)
{
push_back(&mylist,num); //尾插
}
break;
case 2:
printf("push_front: input numbers(-1 erminate):");
while(scanf("%d",&num),num != -1)
{
push_front(&mylist,num); //頭插
}
break;
case 3:
show_list(&mylist);
break;
case 4:
pop_back(&mylist); //尾刪
break;
case 5:
pop_front(&mylist); //頭刪
break;
case 6:
printf("input inset value:>");
scanf("%d",&num);
insert_val(&mylist,num); //升序插值
break;
case 7:
printf("input find value:>");
scanf("%d",&num);
p = find(&mylist,num); //查詢某值
if(p == NULL)
printf("value %d not exist\n",num);
break;
case 8:
printf("length:%d\n",length(&mylist)); //長度
case 9:
printf("input delete value:>");
scanf("%d",&num);
delete_val(&mylist,num); //刪除值
break;
case 10:
sort(&mylist); //升序
break;
case 11:
reverse(&mylist); //反轉
break;
case 12:
clear(&mylist); //清除
break;
case 13:
destroy(&mylist); //銷燬
break;
default:
printf("error,choose again\n");
break;
}
}
printf("Hello world!");
return 0;
}