- 1 高階資料
- 1.1 結構
- 1.2 從陣列到連結串列
- 1.3 抽象資料型別(ADT)
- 1.3.1 講解
- 1.3.2 實踐
- 1.4 佇列ADT
- 1.4.1 講解
- 1.4.2 用佇列進行模擬
- 1.5 連結串列和陣列
- 1.6 二叉查詢樹
- 1.6.1 講解
- 1.6.2 實踐
1 高階資料
1.1 結構
在開始編寫程式碼之前,要做很多程式設計方面的決定。
陣列表示相對不靈活,在執行時確定所需記憶體量會更好。
假設要編寫一個程式,讓使用者輸入一年內看過的電影,儲存影片的資訊。可以使用結構儲存電影,用結構陣列儲存多部電影。但給陣列分配空間時,會出現分配空間過大浪費或者分配空間過小不夠用的問題。使用動態記憶體(malloc)分配可以解決這個問題。
示例程式:
// films1.c -- 使用一個結構陣列
#include <stdio.h>
#include <string.h>
#define TSIZE 45 // 儲存片名的陣列大小
#define FMAX 5 // 影片的最大數量
struct film
{
char title[TSIZE];
int rating;
};
char *s_gets(char str[], int lim);
int main(void)
{
struct film movies[FMAX];
int i = 0;
int j;
puts("Enter first movie title:");
while (i < FMAX && s_gets(movies[i].title, TSIZE) != NULL && movies[i].title[0] != '\0')
{
puts("Enter your rating <0-10>:");
scanf("%d", &movies[i++].rating);
while (getchar() != '\n')
continue;
puts("Enter next movie title (empty line to stop):");
}
if (i == 0)
printf("No data entered. ");
else
printf("Here is the movie list:\n");
for (j = 0; j < i; j++)
printf("Movies: %s Rating: %d\n", movies[j].title, movies[j].rating);
printf("Bye!\n");
return 0;
}
char *s_gets(char *st, int n)
{
char *ret_val;
char *find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n'); // 查詢換行符
if (find) // 如果地址不是NULL
*find = '\0'; // 在此處放置一個空字元
else
while (getchar() != '\n')
continue; // 處理輸入行的剩餘字元
}
return ret_val;
}
點選瞭解更多關於結構資訊
1.2 從陣列到連結串列
結構宣告中不能有與本身型別相同的結構,但是可以有指向同型別結構的指標。
連結串列是由一系列結構體構成,每個結構體都有一個指標,該指標指向下一個結構。最後一個成員中此指標的值是0。
為了訪問連結串列,需要一個單獨的指標儲存第一個成員的地址。
把使用者介面和程式碼細節分開的程式更容易理解和更新。
示例程式:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define TSIZE 45 //片名大小
struct film {
char title[TSIZE];
int rating;
struct film * next; //指向連結串列的下一個結構
};
char * s_gets(char * st, int n);
int main(void)
{
struct film * head = NULL;
struct film * prev = NULL, *current = NULL;
char input[TSIZE];
puts("輸入第一部電影的名字:");
while (s_gets(input, TSIZE) != NULL && input[0] != '\0')
{
current = (struct film *) malloc(sizeof(struct film));
if (head == NULL)
head = current;
else
prev->next = current;
current->next = NULL;
strcpy(current->title, input);
puts("輸入評分<0-10>:");
scanf("%d", ¤t->rating);
while (getchar() != '\n')
continue;
puts("輸入下一部電影名字(直接回車可退出)");
prev = current;
}
//顯示電影
if (head == NULL)
printf("無資料.");
else
{
printf("電影列表如下:\n");
current = head;
while (current != NULL)
{
printf("電影:%s 評分:%d\n", current->title, current->rating);
current = current->next;
}
}
//釋放記憶體
current = head;
while (head != NULL) //此處和書不同,書上執行出錯。我認為這裡應該判斷head是否NULL而不是current是否為NULL
{
current = head;
head =head->next;
free(current);
}
printf("BYE\n");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n');//查詢換行符
if (find)
*find = '\0'; //將換行符換成'\0'
else
while (getchar() != '\n') //處理輸入行剩餘的字元
continue;
}
return ret_val;
}
1.3 抽象資料型別(ADT)
1.3.1 講解
型別特指兩種資訊:屬性
和操作
。要定義一個新的資料型別,就必須提供儲存資料的方法,還有操控資料的方法。
定義新型別的好方法是:先提供型別屬性和相關操作的抽象描述。這些描述不依賴特定的實現,也不依賴特定的程式語言,稱為抽象資料型別(ADT)。再開發一個實現ADT的程式設計介面,指明如何儲存資料和執行所需操作的函式。最後編寫程式碼實現介面。
C語言中通常的做法是,把型別定義和函式原型放在一個標頭檔案中,該標頭檔案提供資訊。實現介面需要一個原始檔,記錄需要函式的細節。程式由標頭檔案、包含處理此型別函式的原始檔和主幹操作的原始檔組成。
對於大型專案而言,把實現和最終介面隔離的做法相當有用。
定義新型別的好方法:
- 提供型別屬性和相關操作的抽象描述。這些描述即不能依賴特定的實現,也不能依賴特定的程式語言。這種正式的抽象描述被稱為抽象資料型別(ADT)。
- 開發一個實現 ADT 的程式設計介面。即指明如何儲存資料和執行所需操作的函式。
- 編寫程式碼實現介面。
1.3.2 實踐
下面是連結串列的具體實現:
//list.h
#pragma once
#include<stdbool.h>
/*特定程式的宣告*/
#define TSIZE 45 //儲存電影名的陣列大小
struct film
{
char title[TSIZE];
int rating;
};
/*一般型別定義*/
typedef struct film Item;
typedef struct node
{
Item item;
struct node * next;
}Node;
typedef Node * List;
/*函式原型*/
/*操作: 初始化一個連結串列 */
/*前提條件: plist指向一個連結串列 */
/*後置條件: 連結串列初始化為空 */
void InitializeList(List * plist);
/*操作: 確定連結串列是否為空定義,plist指向一個已初始化的連結串列 */
/*後置條件: 如果連結串列為空,返回ture;否則返回false */
bool ListIsEmpty(const List * plist);
/*操作: 確定連結串列是否已滿,plist指向一個已初始化的連結串列 */
/*後置條件: 如果連結串列已滿,返回true;否則返回false */
bool ListIsFull(const List * plist);
/*操作: 確定連結串列中的項數,plist指向一個已初始化的連結串列 */
/*後置條件: 返回連結串列中的項數 */
unsigned int ListItemCount(const List *plist);
/*操作: 在連結串列的末尾新增項 */
/*前提條件: item是一個待新增至連結串列的項,plist指向一個已初始化的連結串列 */
/*後置條件: 如果可以,執行新增操作,返回true;否則返回false */
bool AddItem(Item item, List * plist);
/*操作: 把函式作用於連結串列的每一項 */
/* plist指向一個已初始化的連結串列 */
/* pfun指向一個函式,該函式接受一個Item型別引數,無返回值 */
/*後置條件: pfun指向的函式作用於連結串列的每一項一次 */
void Traverse(const List*plist, void(*pfun)(Item item));
/*操作: 釋放已分配的記憶體(如果有的話) */
/* plist指向一個已初始化的連結串列 */
/*後置條件: 釋放為連結串列分配的記憶體,連結串列設定為空 */
void EmptyTheList(List * plist);
//list.c
#include<stdio.h>
#include<stdlib.h>
#include"list.h"
static void CopyToNode(Item item, Node * pnode);
void InitializeList(List * plist)
{
*plist = NULL;
}
bool ListIsEmpty(const List * plist)
{
if (*plist == NULL)
return true;
else
return false;
}
bool ListIsFull(const List * plist)
{
Node * pt;
bool full;
pt = (Node *)malloc(sizeof(Node));
if (pt == NULL)
full = true;
else
full = false;
free(pt);
return full;
}
unsigned int ListItemCount(const List * plist)
{
unsigned int count = 0;
Node * pnode = *plist;
while (pnode != NULL)
{
++count;
pnode = pnode->next;
}
return count;
}
bool AddItem(Item item, List * plist)
{
Node * pnew;
Node * scan = *plist;
pnew = (Node *)malloc(sizeof(Node));
if (pnew == NULL)
return false;
CopyToNode(item, pnew);
pnew->next = NULL;
if (scan == NULL)
*plist = pnew;
else
{
while (scan->next != NULL)
scan = scan->next;
scan->next = pnew;
}
return true;
}
void Traverse(const List * plist, void(*pfun)(Item item))
{
Node * pnode = *plist;
while (pnode!= NULL)
{
(*pfun)(pnode->item);
pnode = pnode->next;
}
}
void EmptyTheList(List * plist)
{
Node * psave;
while (*plist != NULL)
{
psave = (*plist)->next;
free(*plist);
*plist = psave;
}
}
static void CopyToNode(Item item, Node * pnode)
{
pnode->item = item;
}
示例程式:
/*film3.c */
/*與list.c一起編譯 */
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include"list.h"
void showMovies(Item item);
char * s_gets(char * st, int n);
int main(void)
{
List movies;
Item temp;
/*初始化 */
InitializeList(&movies);
if (ListIsFull(&movies))
{
fprintf(stderr, "無可用記憶體,告辭。\n");
exit(1);
}
/*獲取使用者輸入 並儲存*/
puts("輸入第一個電影名稱:");
while (s_gets(temp.title, TSIZE) != NULL && temp.title[0] != '\0')
{
puts("輸入你的評分<0-10>:");
scanf("%d", &temp.rating);
while (getchar() != '\n')
continue;
if (AddItem(temp, &movies) == false)
{
fprintf(stderr, "分配記憶體出錯\n");
break;
}
if (ListIsFull(&movies))
{
puts("列表滿了.");
break;
}
puts("輸入下一步電影名稱(回車結束程式)");
}
/*顯示*/
if (ListIsEmpty(&movies))
printf("列表為空");
else
{
printf("Here is the movie list:\n");
Traverse(&movies, showMovies);
}
printf("你輸入了%d個電影\n", ListItemCount(&movies));
/*清理*/
EmptyTheList(&movies);
printf("再見\n");
return 0;
}
void showMovies(Item item)
{
printf("Movie: %s Rating: %d\n", item.title, item.rating);
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n');//查詢換行符
if (find)
*find = '\0'; //將換行符換成'\0'
else
while (getchar() != '\n') //處理輸入行剩餘的字元
continue;
}
return ret_val;
}
1.4 佇列ADT
1.4.1 講解
佇列是具有一些特殊屬性的連結串列,新項只能新增到連結串列的末尾,只能從連結串列的開頭移除項。佇列先進先出。
1.4.2 用佇列進行模擬
佇列特性:先進先出。
示例程式:
// mall.c -- 使用Queue介面
// 和queue.c一起編譯
#include <stdio.h>
#include <stdlib.h> // 提供rand()和srand()的原型
#include <time.h> // 提供time()的原型
#include "17_6_queue.h" // 更改Item的typedef
#define MIN_PER_HR 60.0
bool newcustomer(double x); // 是否有新顧客到來?
Item customertime(long when); // 設定顧客引數
int main(void)
{
Queue line; // 新的顧客資料
Item temp; // 模擬的小時數
int hours; // 每小時平均多少位顧客
int perhour; // 每小時平均多少位顧客
long cycle, cyclelimit; // 迴圈計數器、計數器的上限
long turnaways = 0; // 因佇列已滿被拒的顧客數量
long customers = 0; // 加入佇列的顧客數量
long served = 0; // 在模擬期間諮詢過Sigmund的顧客數量
long sum_line = 0; // 累計的佇列總長
long wait_time = 0; // 從當前到Sigmund空閒所需的時間
double min_per_cust; // 顧客到來的平均時間
long line_wait = 0; // 佇列累計的等待時間
InitializeQueue(&line);
srand((unsigned int)time(0)); // rand()隨機初始化
puts("Case Study: Sigmund Lander's Advice Booth");
puts("Enter the number of simulation hours:");
scanf("%d", &hours);
cyclelimit = MIN_PER_HR * hours;
puts("Enter the average number of customers per hour:");
scanf("%d", &perhour);
min_per_cust = MIN_PER_HR / perhour;
for (cycle = 0; cycle < cyclelimit; cycle++)
{
if (newcustomer(min_per_cust))
{
if (QueueIsFull(&line))
turnaways++;
else
{
customers++;
temp = customertime(cycle);
EnQueue(temp, &line);
}
}
if (wait_time <= 0 && !QueueIsEmpty(&line))
{
DeQueue(&temp, &line);
wait_time = temp.processtime;
line_wait += cycle - temp.arrive;
served++;
}
if (wait_time > 0)
wait_time--;
sum_line += QueueItemCount(&line);
}
if (customers > 0)
{
printf("customers accepted: %ld\n", customers);
printf(" customers served: %ld\n", served);
printf(" turnaways: %ld\n", turnaways);
printf("average queue size: %.2f\n", (double)sum_line / cyclelimit);
printf(" average wait time: %.2f minutes\n", (double)line_wait / served);
}
else
puts("No customers!");
EmptyTheQueue(&line);
return 0;
}
// x是顧客到來的平均時間(單位:分鐘)
// 如果1分鐘內有顧客到來,則返回true
bool newcustomer(double x)
{
if (rand() * x / RAND_MAX < 1)
return true;
else
return false;
}
// when是顧客到來的時間
// 該函式返回一個Item結構,該顧客到達的時間設定為when
// 諮詢時間設定為1~3的隨機值
Item customertime(long when)
{
Item cust;
cust.processtime = rand() % 3 + 1;
cust.arrive = when;
return cust;
}
1.5 連結串列和陣列
陣列是C語言直接支援的,可以隨機訪問,但是陣列在編譯時就確定大小,插入和刪除元素很麻煩。連結串列執行時確定大小,插入刪除很方便,但是不能隨機訪問,開發難度大。
對於一個排序的列表,二分查詢的效率比順序查詢要高得多。二分查詢把所有元素分為一半,比中間的小就去前半部分,比中間元素大就去後半部分,與中間的相等就算找到了,進入前半或後半部分後以此類推。
如果經常使用增刪操作,使用連結串列更好。如果經常查詢,陣列更好。
陣列和連結串列優缺點:
資料形式 | 優點 | 缺點 |
---|---|---|
陣列 | C直接支援;提供隨機訪問 | 在編譯時確定大小;插入和刪除元素時很費時 |
連結串列 | 執行時確定大小;快速插入和刪除元素 | 不能隨機訪問;使用者必須提供程式設計支援 |
1.6 二叉查詢樹
1.6.1 講解
二叉樹的每個節點有兩個指標,這兩個指標指向其他節點(分別稱為左節點
和右節點
)
一般左節點在的項在父節點前面,右節點的項在父節點後面。如果一側沒有子節點,則指向這一側的指標為NULL。二叉樹的頂端稱為根。一個節點和它的所有節點構成子樹。
用二叉樹每次查詢就會排除一半的節點,效率高,但是更復雜。
1.6.2 實踐
// tree.h -- 二叉查詢樹
// 樹種不允許有重複的項
#ifndef _TREE_H_
#define _TREE_H_
#include <stdbool.h>
// 根據具體情況重新定義Item
#define SLEN 20
typedef struct item
{
char petname[SLEN];
char petkind[SLEN];
} Item;
#define MAXITEMS 10
typedef struct trnode
{
Item item;
struct trnode *left; // 指向左分支的指標
struct trnode *right; // 指向右分支的指標
} Trnode;
typedef struct tree
{
Trnode *root; // 指向根節點的指標
int size; // 樹的項數
} Tree;
// 函式原型
// 操作: 把樹初始化為空
// 前提條件: ptree指向一個樹
// 後置條件: 樹被初始化為空
void InitializeTree(Tree *ptree);
// 操作: 確定樹是否為空
// 前提條件: ptree指向一個樹
// 後置條件: 如果樹為空,該函式返回true,否則返回false
bool TreeIsEmpty(const Tree *ptree);
// 操作: 確定樹是否已滿
// 前提條件: ptree指向一個樹
// 後置條件: 如果樹已滿,該函式返回true,否則返回false
bool TreeIsFull(const Tree *ptree);
// 操作: 確定樹的項數
// 前提條件: ptree指向一個樹
// 後置條件: 返回樹的項數
int TreeItemCount(const Tree *ptree);
// 操作: 在樹中新增一個項
// 前提條件: pi是待新增項的地址,ptree指向一個一初始化的樹
// 後置條件: 如果可以新增,該函式將在樹中新增一個項並返回true,否則返回false
bool AddItem(const Item *pi, Tree *ptree);
// 操作: 在樹中查詢一個項
// 前提條件: pi指向一個項,ptree指向一個已初始化的樹
// 後置條件: 如果在樹中新增一個項,該函式返回true,否則返回false
bool InTree(const Item *pi, const Tree *ptree);
// 操作: 從樹中刪除一個項
// 前提條件: pi是刪除項的地址,ptree指向一個已初始化的樹
// 後置條件: 如果從樹中成功刪除一格項,該函式返回true,否則返回false
bool DeleteItem(const Item *pi, Tree *ptree);
// 操作: 把函式應用到樹中的每一項
// 前提條件: ptree指向一個樹,pfun指向一個函式,該函式接收一個Item型別的引數,並無返回值
// 後置條件: pfun咋想的這個函式為樹中的每一項執行一次
void Traverse(const Tree *ptree, void (*pfun)(Item item));
// 操作: 刪除樹中的所有內容
// 前提條件: ptree指向一個已初始化的樹
// 後置條件: 樹為空
void DeleteAll(Tree *ptree);
// tree.c -- 樹的支援函式
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "17_10_tree.h"
// 區域性資料型別
typedef struct pair
{
Trnode *parent;
Trnode *child;
} Pair;
// 區域性函式的原型
static Trnode *MakeNode(const Item *pi);
static bool ToLeft(const Item *i1, const Item *i2);
static bool ToRight(const Item *i1, const Item *i2);
static void AddNode(Trnode *new_node, Trnode *root);
static void InOrder(const Trnode *root, void (*pfun)(Item item));
static Pair SeekItem(const Item *pi, const Tree *ptree);
static void DeleteNode(Trnode **ptr);
static void DeleteAllNodes(Trnode *ptr);
// 函式定義
void InitializeTree(Tree *ptree)
{
ptree->root = NULL;
ptree->size = 0;
}
bool TreeIsEmpty(const Tree *ptree)
{
if (ptree->root == NULL)
return true;
else
return false;
}
bool TreeIsFull(const Tree *ptree)
{
if (ptree->root == NULL)
return true;
else
return false;
}
int TreeItemCount(const Tree *ptree)
{
if (ptree->size == MAXITEMS)
return true;
else
return false;
}
bool AddItem(const Item *pi, Tree *ptree)
{
Trnode *new_node;
if (TreeIsFull(ptree))
{
fprintf(stderr, "Tree is full\n");
return false; // 提前返回
}
if (SeekItem(pi, ptree).child != NULL)
{
fprintf(stderr, "Attempted to add duplicate item\n");
return false; // 提前返回
}
new_node = MakeNode(pi); // 指向新節點
if (new_node == NULL)
{
fprintf(stderr, "Couldn't create node\n");
return false; // 提前返回
}
// 成功建立了一個新節點
ptree->size++;
if (ptree->root == NULL) // 情況1:樹為空
ptree->root = new_node; // 新節點為樹的根節點
else // 情況2:樹不為空
AddNode(new_node, ptree->root); // 在樹中新增新節點
return true; // 成功返回
}
bool InTree(const Item *pi, const Tree *ptree)
{
return (SeekItem(pi, ptree).child == NULL) ? false : true;
}
bool DeleteItem(const Item *pi, Tree *ptree)
{
Pair look;
look = SeekItem(pi, ptree);
if (look.child == NULL)
return false;
if (look.parent == NULL) // 刪除根節點項
DeleteNode(&ptree->root);
else if (look.parent->left == look.child)
DeleteNode(&look.parent->left);
else
DeleteNode(&look.parent->right);
ptree->size--;
return true;
}
void Traverse(const Tree *ptree, void (*pfun)(Item item))
{
if (ptree != NULL)
InOrder(ptree->root, pfun);
}
void DeleteAll(Tree *ptree)
{
if (ptree != NULL)
DeleteAllNodes(ptree->root);
ptree->root = NULL;
ptree->size = 0;
}
// 區域性函式
static void InOrder(const Trnode *root, void (*pfun)(Item item))
{
if (root != NULL)
{
InOrder(root->left, pfun);
(*pfun)(root->item);
InOrder(root->right, pfun);
}
}
static void DeleteAllNodes(Trnode *root)
{
Trnode *pright;
if (root != NULL)
{
pright = root->right;
DeleteAllNodes(root->left);
free(root);
DeleteAllNodes(pright);
}
}
static void AddNode(Trnode *new_node, Trnode *root)
{
if (ToLeft(&new_node->item, &root->item))
{
if (root->left == NULL) // 空子樹
root->left = new_node; // 把結點新增到此處
else
AddNode(new_node, root->left); // 否則處理該子樹
}
else if (ToRight(&new_node->item, &root->item))
{
if (root->right == NULL) // 空子樹
root->right = new_node; // 把結點新增到此處
else
AddNode(new_node, root->right); // 否則處理該子樹
}
else // 不允許有重複項
{
fprintf(stderr, "location error in AddNode()\n");
exit(1);
}
}
static bool ToLeft(const Item *i1, const Item *i2)
{
int comp1;
if ((comp1 = strcmp(i1->petname, i2->petname)) < 0)
return true;
else if (comp1 == 0 && strcmp(i1->petkind, i2->petkind) < 0)
return true;
else
return false;
}
static bool ToRight(const Item *i1, const Item *i2)
{
int comp1;
if ((comp1 = strcmp(i1->petname, i2->petname)) > 0)
return true;
else if (comp1 == 0 && strcmp(i1->petkind, i2->petkind) > 0)
return true;
else
return false;
}
static Trnode *MakeNode(const Item *pi)
{
Trnode *new_node;
new_node = (Trnode *)malloc(sizeof(Trnode));
if (new_node != NULL)
{
new_node->item = *pi;
new_node->left = NULL;
new_node->right = NULL;
}
return new_node;
}
static Pair SeekItem(const Item *pi, const Tree *ptree)
{
Pair look;
look.parent = NULL;
look.child = ptree->root;
if (look.child == NULL)
return look; // 提前返回
while (look.child == NULL)
{
if (ToLeft(pi, &(look.child->item)))
{
look.parent = look.child;
look.child = look.child->left;
}
else if (ToRight(pi, &(look.child->item)))
{
look.parent = look.child;
look.child = look.child->right;
}
else // 如果前兩種情況都不滿足,則必定是相等的情況
break; // look.child目標項的結點
}
return look; // 成功返回
}
static void DeleteNode(Trnode **ptr) // ptr是指向目標節點的父節點指標成員的地址
{
Trnode *temp;
if ((*ptr)->left == NULL)
{
temp = *ptr;
*ptr = (*ptr)->right;
free(temp);
}
else if ((*ptr)->right == NULL)
{
temp = *ptr;
*ptr = (*ptr)->left;
free(temp);
}
else // 被刪除的結點有兩個子節點
{
// 找到重新連線右子樹的位置
for (temp = (*ptr)->left; temp->right != NULL; temp = temp->right)
continue;
temp->right = (*ptr)->right;
temp = *ptr;
*ptr = (*ptr)->left;
free(temp);
}
}