鏈表
對于確定長度的同類型數據,之前學習了如何用數組存儲。對于長度不確定的、經常要改變的數據,我們則會選擇構造一個被稱為 鏈表(linked list) 的結構。
鏈表由一系列存儲了數據的節點和他們之間的指向關系構成。從第一個節點(鏈表頭)開始,每一個節點都會指向下一個節點,直到鏈表末端的一個節點為止。
C 語言通常會定義一個結構體,來對鏈表節點進行實現。在這結構體中,有一系列數據,同時還有一個指向這一結構體類型的指針。
在這種定義下,我們就可以通過將第二個節點的地址保存在第一個節點的指針變量中的方式實現節點之間的指向了。
相對于數組來說,鏈表有一定的優勢和劣勢。
- 優勢
存儲的元素數不固定,數據結構所需的內存空間無需一開始就說明。
只要改變指針指向就可以動態地在兩個節點之間插入數據。
- 劣勢
在內存中不連續,查詢效率沒有數組高,需要從鏈表頭開始沿著指針方向依次訪問每一個節點才能到達目標節點。
通過鏈表節點構造一個鏈表
在這里,我們沒有太深入地對鏈表進行操作,但是可以想一下如果我們要刪除一個節點、插入一個節點分別應該怎么辦。
刪除一個節點,我們需要從head開始,沿著->next逐一地到達目標節點。將目標節點的前一節點的->next改為目標節點當前的->next內保存的值。使用free將目標節點對應的內存空間釋放。
插入一個節點則需要創建一個新的節點,將鏈表目標位置前的一個節點的->next修改為新的節點的地址,并將新的節點的->next插入目標位置前的一個節點的原->next取值。
值得注意的是,在這一課的代碼中,我們在堆區內存上開了空間,但是沒有釋放。從內存安全的角度出發,我們應該還要在鏈表被使用完畢后釋放所有開辟給鏈表節點的堆區上內存喔。
如果我們將鏈表的頭尾相連,我們還可以得到一個環形的鏈表,被稱為“循環鏈表”。
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int number;
struct node *next;
} Node;
鏈表的性質(判斷):
問題:約瑟夫環
學習鏈表結構后,需要用鏈表解決一個稍有改動的“約瑟夫環(Josephus problem)”問題:
有N個同學圍成一圈,順序編號(分別為 1,2,3...n),從編號為 K的人開始報 1,他之后(順初始數字增長方向計算序號)的人報 2,數到某個數字 M 的人出列。出列同學的下一人又從 1繼續報,數到某個數 M 的人出列。重復直到所有人出列。
現需要根據同學人數 N和給出的 K 和 M 計算出同學的正確出列順序。
- 輸入格式
輸入為一行,包括三個被空格分隔開的符合描述的正整數 N、K和 M
(1≤K≤N≤1000,1≤M≤2000)。 - 輸出格式
輸出為一行,包含 N個整數,為依次順序出列的學生編號,由空格分隔開。
樣例輸入1
9 1 1
樣例輸出1
1 2 3 4 5 6 7 8 9
樣例輸入2
8 5 2
樣例輸出2
6 8 2 4 7 3 1 5
思路:在刪除循環鏈表節點的過程中,一般是先設置臨時指針,然后對臨時指針循環一圈,讓 臨時指針 在 當前的頭指針 的前面,具體原理:鏈表修改的原理,臨時指針的next越過下節點,指向下下節點。同時將刪除的節點元素的內存釋放出來。
遇到的坑主要有置空以后又引用,引起的段錯誤。
代碼如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node *next;
} Node; //構造鏈表結構體
Node *circle_create(int n); //創建環的函數
void count_off(Node *head, int n, int k, int m); //約瑟夫環出列函數
int main() {
int n, k, m;
scanf("%d%d%d", &n, &k, &m);
Node *head = circle_create(n);
count_off(head, n, k, m);
return 0;
}
Node *circle_create(int n) {
Node *temp, *new_node, *head;
int i;
// 創建第一個鏈表節點并加數據
temp = (Node *) malloc(sizeof(Node)); //臨時申請空間
head = temp; //申請到的空間賦給頭元素
head->data = 1;
// 創建第 2 到第 n 個鏈表節點并加數據
for(i = 2; i <= n; i++) { //”創指針“不斷開辟新空間,利用temp指針進行跟隨
new_node = (Node *) malloc(sizeof(Node)); //”創指針“申請臨時空間
new_node->data = i;
temp->next = new_node; //臨時節點節點指向”創指針“
temp = new_node; //”創指針“賦給臨時節點
}
// 最后一個節點指向頭部構成循環鏈表
temp->next = head;
return head;
}
void count_off(Node *head, int n, int k, int m) {
Node *temp;
temp = head;
int i = 1;
while(i<=n){
temp = head;
head = head->next;
i++;
} //置temp到head前一位
i = 1;
while(i<k){
temp = head;
head = head->next;
i++;
} //head移到k號位置,temp移到k-1號位
i = 1;
while (1){
while(i< m){
temp = head;
head = head->next; //報m號,m號為head,temp則在head前一位
i++;
}
i = 1;
if (temp!= head){
printf("%d ",head->data); //輸出m號,即head節點內容
temp->next = head->next; //這里把temp指向下下個節點
free(head); //輸出完了釋放head的內存
head = temp->next; //現在head就得是剛才head的下一位了
}else {
printf("%d",head->data);
free(head);
break; //實際上還應該置野指針為NULL
}
}
return;
}
共用體
它看起來和結構體很像的東西,稱為 共用體(union)。
結構體特性解決了一系列不同類型的變量可以怎么被放在一起組織的問題;
而共用體,則使多種不會同時出現的變量共用同一塊內存,十分方便。
共用體初始化:
定義共用體的關鍵字是union,一個共用體可以包括多個合法的類型描述成員,例如:
union register {
struct {
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
} bytes;
unsigned int number;
};
這共用體所占用的內存空間是被公用的,可通過struct
類型的bytes
和unsigned int
類型的number
兩種不同的類型描述成員進行訪問。
無論我們通過哪一種描述成員訪問這一共用體,我們訪問的都會是同一塊內存空間。
如果用這個union register類型聲明一個變量reg。我們將可以通過reg.bytes按字節訪問或者通過reg.number整體訪問兩種不同的方式獲得或修改同一片內存。
共用體在涉及到直接操作內存的嵌入式編程、需要極度節省空間的通信協議設計中都會有它的優勢。
在之前的內容中,我們看到了一種通過共用體實現的可以整體修改,也可以按字節修改的類型。類似的,我們也可以定義一個既可以按位訪問,也可以按字節訪問的類型:
union {
struct {
unsigned char b1:1;
unsigned char b2:1;
unsigned char b3:1;
unsigned char b4:1;
unsigned char reserved:4;
} bits;
unsigned char byte;
}
這里有一個冒號是用來定義變量使用內存的“位長”的。這里:1
、:4
表示冒號前的變量只需要占 1 個和 4 個二進制位,而不按照char類型默認的字節數占用內存。這樣,用這個類型生成的變量就可以被我們就按字節或者按位進行訪問和使用了(這個概念被稱為 位域(bit field),在其它場景下也可以被使用)。
再舉一個被設計出來專門儲存IP地址的共用體結構。使用了它的變量,既可以存儲 IPv4 的 IP 地址,也可以存儲 IPv6 的 IP 地址,這些地址既可以作為一個整體被操作,也可以分幾個部分分別操作。
union {
// IPv4 地址
union {
struct {
unsigned char _reserved[12];
unsigned char _ip_bytes[4];
} _raw;
struct {
unsigned char _reserved[12];
unsigned char _o1;
unsigned char _o2;
unsigned char _o3;
unsigned char _o4;
} _octet;
} _IPv4;
// IPv6 地址
union {
struct {
unsigned char _IpBytes[16];
} _raw;
struct {
unsigned short _w1;
unsigned short _w2;
unsigned short _w3;
unsigned short _w4;
unsigned short _w5;
unsigned short _w6;
unsigned short _w7;
unsigned short _w8;
} _word;
} _IPv6;
} _IP;
共用體其實也可以被視為一種特殊的結構體,但是一般的結構體中的成員會在內存中對齊排列(如果你對這塊有興趣可以在互聯網上通過搜索多了解一些,我們在這里不做過多介紹),而共用體則都選擇以同一位置作為起點,共用同一開始地址的內存。
- 共用體類型的變量占用內存的大小將會和他所有成員中占用內存的最大的一致。
- 類型別名在共用體類型上的使用方式與在其他類型上相同,沒有區別。
- 和其他類型一樣,共用體類型也可以被用于結構體類型定義。
- 和其他類型一樣,共用體類型也可以被用于結構體類型定義。
驗證共用體類型的變量取出的內存地址是不是完全一致
#include <stdio.h>
#include <stdlib.h>
typedef union type_x {
char a;
int b;
float c;
} Type_x;
typedef struct type_y {
char a;
int b;
float c;
} Type_y;
int main() {
Type_x x;
Type_y y;
printf("%p %p %p\n", &(x.a),&(x.b),&(x.c));
printf("%p %p %p\n", &(y.a),&(y.b),&(y.c));
return 0;
}
可能的運行結果:
0x7ffd44294e40 0x7ffd44294e40 0x7ffd44294e40
0x7ffd44294e50 0x7ffd44294e54 0x7ffd44294e58
枚舉
C 語言提供了一種數據類型叫 枚舉(enumeration)。由一系列整數成員組成,表示這一數據類型的變量可以取的所有可能值;但這些值都不直接以字面量形式存在,每一個值都被單獨給予了一個名字。例如:
enum week {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
這種方式定義了一個與周相關的枚舉類型,其中每一個成員都會對應一個編號。
當像上面例子這樣沒有對它們進行顯性的編號時,系統默認編號會從 0開始。也就是如果直接使用
SUNDAY
,將和使用0
一樣,而使用MONDAY
則會相當于使用了1
,依此類推。也可以給枚舉類型成員進行顯性的編號。如果我們給
SUNDAY
編號為1
enum week {
SUNDAY = 1,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
我們使用MONDAY
則會相當于使用了2
,每一個成員都比之前編號多1
- 也可以給多個枚舉類型成員進行顯性的編號。
enum week {
SUNDAY = 1,
MONDAY,
TUESDAY,
WEDNESDAY = 1,
THURSDAY,
FRIDAY,
SATURDAY
};
當將SUNDAY
和WEDNESDAY
都編號為1
的時候,使用MONDAY
或者使用THURSDAY
則都會相當于使用了2
。
不難發現, 其實可以對任何一個枚舉成員進行顯性的編號,那么:
沒有顯性編號的其他成員的編號將從它之前一個顯性編號的成員開始計算,按順序依次加一獲得。
當一個枚舉類型被定義以后,我們可以直接用這一類型聲明變量。如:
enum week {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
enum week exam_date;
聲明了一個enum week
類型的變量exam_date
,它只能取定義過的枚舉類型中的成員名作為值,如exam_date = TUESDAY;
。
與struct
、union
以及其它類型一樣,我們也可以給枚舉類型通過typedef
設置類型別名。
輸出枚舉類型舉例:
#include <stdio.h>
typedef enum week {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
} Week;
int main() {
Week meeting_date;
meeting_date = FRIDAY;
printf("%d\n", meeting_date);
return 0;
}
- 枚舉類型中成員的編號只能是整數。