高級篇
數據結構
C語言標準庫是沒有提供數據結構的,但數據結構是編程中的基礎設施,其他編程語言通常都是自帶各種數據結構。這裏我們簡單實現一下,將數據結構的基礎知識與C語言語法綜合練習一下。
線性表
線性表是最爲常用的數據結構之一,其他高級語言也都有提供,也就是Java、Python中的List
基於數組
基於數組的線性表就是一個動態數組,可以自動增長。這裏以int
類型元素爲例,如需實現泛型,可以參考上一篇的void*
指針。
頭文件arraylist.h
#ifndef _ARRAY_LIST_H_
#define _ARRAY_LIST_H_
// 默認容量
#define DEFAULT_CAPACITY 8
#define OK 0
#define ERROR -1
typedef int Element;
// 聲明動態列表的結構體
typedef struct
{
Element *container;
int length; // 列表長度
int capacity; // 底層數組容量
} ArrayList;
// 初始化動態列表
int AL_init(ArrayList *);
// 添加元素
int AL_add(ArrayList*,Element);
// 刪除元素
int AL_remove(ArrayList*,int);
// 獲取元素
int AL_get(ArrayList*,int,Element*);
#endif
源碼arraylist.c
#include "arraylist.h"
#include <stdlib.h>
int AL_init(ArrayList *lst){
if (lst == NULL){
return ERROR;
}
lst->length = 0;
lst->capacity = DEFAULT_CAPACITY;
lst->container = calloc(DEFAULT_CAPACITY,sizeof(int));
if (lst->container == NULL){
return ERROR;
}
return 0;
}
int AL_add(ArrayList *lst,Element element){
if (lst == NULL){
return ERROR;
}
if (lst->length < lst->capacity){
lst->container[lst->length] = element;
lst->length ++;
}else{
int newSize = lst->capacity*2;
int *tmp = realloc(lst->container, newSize*sizeof(int));
if (tmp == NULL){
printf("realloc error\n");
return ERROR;
}
lst->capacity = newSize;
lst->container = tmp;
lst->container[lst->length] = element;
lst->length ++;
}
return OK;
}
int AL_remove(ArrayList *lst,int position){
if (lst == NULL || position >= lst->length){
return ERROR;
}
for (int i = position; i < lst->length-1; i++){
lst->container[i] = lst->container[i+1];
}
lst->length --;
return OK;
}
int AL_get(ArrayList *lst,int position,Element *element){
if (position < lst->length){
*element = lst->container[position];
return OK;
}else{
return ERROR;
}
}
創建測試代碼 test.c
#include <stdio.h>
#include "arraylist.h"
int main(){
ArrayList list;
// 初始化列表
int r = AL_init(&list);
// 循環添加元素
for (int i = 0; i < 20; i++){
AL_add(&list, i*2);
}
// 獲取元素並打印
Element e;
for (size_t i = 0; i < list.length; i++){
AL_get(&list,i,&e);
printf("%d\n",e);
}
// 刪除指定位置的元素
AL_remove(&list,3);
AL_remove(&list,4);
AL_remove(&list,5);
printf("-----------------------*-*-----------------------\n");
printf("list size is %d\n",list.length);
printf("-----------------------*-*-----------------------\n");
// 遍歷列表
for (size_t i = 0; i < list.length; i++){
AL_get(&list,i,&e);
printf("%d\n",e);
}
}
gcc編譯命令: gcc arraylist.c test.c -o test
需要說明的是,基於數組實現線性表,當刪除元素時,被刪除元素之後的所有元素都需要向前移動。這就像排隊一樣,如果隊伍中一人突然離開,那麼其後的所有人都需要向前走一步。
基於鏈表
除了基於數組實現,還能通過結構體基於鏈式來實現。所謂鏈式,就和鐵鏈子一樣,一環扣一環。想像一下一羣人手拉手站成一排的樣子,假如中間有A、B、C三人,A拉着B,B拉着C,這時候如果B想要離開,那麼A、C就需要同時鬆開手,B離開後,A和C的手再拉在一起。
這裏我們簡單實現一下單向鏈表的代碼,僅做原理演示
頭文件linkedlist.h
#ifndef _LINKED_LIST_H_
#define _LINKED_LIST_H_
#define OK 0
#define ERROR -1
typedef int Element;
// 聲明節點的結構體
typedef struct _node
{
Element data;
struct _node *next;
} Node;
// 聲明鏈表結構體
typedef struct
{
int length;
Node *link;
} LinkedList;
int LL_init(LinkedList*);
int LL_add(LinkedList *, Element);
int LL_remove(LinkedList*,int);
int LL_get(LinkedList*,int,Element*);
#endif
源文件linkedlist.c
#include "linkedlist.h"
#include <stdlib.h>
// 聲明頭節點。靜態變量,具有當前文件作用域
static Node *head = NULL;
int LL_init(LinkedList *list){
if (list == NULL){
return ERROR;
}
list->link = head;
list->length = 0;
return OK;
}
int LL_add(LinkedList *list, Element e){
if (list == NULL){
return ERROR;
}
Node *node = (Node*)malloc(sizeof(Node));
if (node == NULL){
return ERROR;
}
node->data = e;
node->next = NULL;
if (list->link == NULL){
head = node;
list->link = head;
}else{
list->link->next = node;
list->link = node;
}
list->length ++;
return OK;
}
int LL_remove(LinkedList *list,int pos){
if (list == NULL || pos >= list->length){
return ERROR;
}
Node *pre = head , *cur=NULL;
for (int i = 1; i < pos && pre!=NULL; i++) {
pre = pre->next;
}
if (pre == head){
cur = pre;
}else{
cur = pre->next;
}
pre->next = cur->next;
list->length --;
if (cur !=NULL){
free(cur);
}
return OK;
}
int LL_get(LinkedList *list,int pos,Element *e){
if (list == NULL || pos >= list->length){
return ERROR;
}
Node *cur = head;
for (int i = 0; i < pos && cur!=NULL; i++) {
cur = cur->next;
}
*e = cur->data;
return OK;
}
創建測試代碼 test.c
#include <stdio.h>
#include "linkedlist.h"
int main(){
LinkedList list;
// 初始化鏈表
int r = LL_init(&list);
// 循環添加元素
for (int i = 0; i < 10; i++){
LL_add(&list, i);
}
Element e;
for (size_t i = 0; i < list.length; i++){
LL_get(&list,i,&e);
printf("%d\n",e);
}
LL_remove(&list,9);
LL_remove(&list,5);
printf("-----------------------*-*-----------------------\n");
printf("list size is %d\n",list.length);
printf("-----------------------*-*-----------------------\n");
for (size_t i = 0; i < list.length; i++){
LL_get(&list,i,&e);
printf("%d\n",e);
}
}
基於數組和基於鏈式的線性表各有特點,這裏做一個線性表小結
- 基於數組的線性表,添加、刪除元素性能較差,根據以上代碼可知,當頻繁添加或刪除元素時,需要底層動態數組不斷的申請或移動內存,而頻繁申請堆內存是非常耗費性能的,但是基於數組的線性表,其具有數組的快速索引特點,查詢定位元素時速度非常快
- 基於鏈式的線性表,其查詢元素時較爲耗費性能,且與查詢的元素所處的位置相關。當鏈表元素越多時,被查詢的元素越靠後,其速度越慢。這是因爲鏈表的查詢必須從頭節點開始遍歷,如果被查詢的元素正好是最後一個,那麼就需要將前面所有節點遍歷一次。相對的,鏈表添加、刪除元素的性能非常高,因爲它不需要移動內存,只需要將指針指向一個新的元素。
鏈表的經典運用
先來看一道算法題:
給出兩個 非空 的鏈表用來表示兩個非負的整數。其中,它們各自的位數是按照 逆序 的方式存儲的,並且它們的每個節點只能存儲 一位 數字。
如果,我們將這兩個數相加起來,則會返回一個新的鏈表來表示它們的和。
您可以假設除了數字 0 之外,這兩個數都不會以 0 開頭。
題幹:
/**
* 結構體原型
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){
}
示例:
輸入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
輸出:7 -> 0 -> 8
實際表示:342 + 465 = 807
這個題的坑在於沒有明確說明不限制非負整數的位數。實際上30位、40位的整數都是可以的。這樣一來,我們就不能去考慮常規的加法運算了,因爲直接計算幾十位的整數加法,明顯超出了C語言整型的範圍,溢出了。換個角度,其實就是在問的,超大整數如何在計算機中去表示、去處理、去運算。
爲了方便測試和驗證,我們先自行實現一下題目中的結構體,並填充一些測試數據
struct ListNode {
int val;
struct ListNode *next;
};
// 初始化頭節點
struct ListNode * LL_init(int e){
struct ListNode *node = (struct ListNode*)malloc(sizeof(struct ListNode));
if (node == NULL) return NULL;
node->val = e;
node->next = NULL;
return node;
}
// 添加數字
void LL_add(struct ListNode *list, int e){
while (list->next != NULL) list = list->next;
struct ListNode *node = (struct ListNode*)malloc(sizeof(struct ListNode));
if (node == NULL) return;
node->next = NULL;
node->val = e;
list->next = node;
}
// 測試
int main(){
struct ListNode *list1 = LL_init(2);
LL_add(list1, 4);
LL_add(list1, 3);
struct ListNode *list2 = LL_init(5);
LL_add(list2, 6);
LL_add(list2, 4);
struct ListNode *result = addTwoNumbers(list1,list2);
while (result !=NULL){
printf("%d\n",result->val);
result = result->next;
}
return 0;
}
Ok,準備妥當之後,就差實現題目中的addTwoNumbers
函數了,接下來就實現該算法
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){
int n1,n2,tmp,carry = 0;
struct ListNode *result = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* pResult= result;
result->next = NULL;
while (l1 != NULL ||l2 !=NULL){
if(l1 != NULL){
n1 = l1->val;
l1 = l1->next;
}else n1 = 0; // 如果l1的位都遍歷完了,l2還有位沒有遍歷,則接下來遍歷中,l1的位都用0替代
if(l2 != NULL) {
n2 = l2->val;
l2 = l2->next;
} else n2 = 0;
// 將每個位的數字相加,carry表示是否需要進位
tmp = n1 + n2 + carry;
// 結果大於9,說明需要進位
if (tmp > 9) carry = 1;
else carry = 0;
// 相加的結果模以10,得到運算後這一位置的數字,存入結果鏈表中
if (pResult != NULL) pResult->val = tmp % 10;
if (l1 != NULL ||l2 !=NULL){
pResult->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pResult = pResult->next;
pResult->next = NULL;
}
}
// 所有位遍歷結束後,如還存在進位,就將最後的結果再進一位
if (carry){
pResult->next = (struct ListNode*)malloc(sizeof(struct ListNode));
pResult = pResult->next;
pResult->val = 1;
pResult->next = NULL;
}
return result;
}
打印結果:
7
0
8
修改測試用例爲:11 + 9999999999
int main(){
struct ListNode *list1 = LL_init(1);
LL_add(list1, 1);
struct ListNode *list2 = LL_init(9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
LL_add(list2, 9);
struct ListNode *result = addTwoNumbers(list1,list2);
// …………………省略…………………
}
打印結果:
0
1
0
0
0
0
0
0
0
0
1
棧
棧在我們生活中其實也很常見,例如名片盒,圓桶裝薯片,我們取東西的時候只能先從頂部取,而放的時候則是先從底部開始放,這種結構的典型特徵就是先進後出。關於棧結構,最形象的例子就是槍的彈夾
棧的簡單實現
棧的實現也可以分爲數組和鏈式,其中數組實現是最簡單的,這裏棧實現就以數組爲例,基於鏈式的棧實現可以參照上面的鏈表示例。
頭文件stack.h
#ifndef _STACK_H_
#define _STACK_H_
#define DEFAULT_CAPACITY 10
typedef int boolean;
#define False 0
#define True 1
typedef struct
{
int top;
char **buf;
int capacity;
} Stack;
int initStack(Stack*);
void push(Stack*,char*);
char *pop(Stack*);
boolean isEmpty(Stack*);
void destroy(Stack**);
#endif
源文件stack.c
#include "stack.h"
#include <stdlib.h>
int initStack(Stack* stack){
if (stack == NULL){
return -1;
}
stack->capacity = DEFAULT_CAPACITY;
stack->top = -1;
stack->buf = (char**)calloc(DEFAULT_CAPACITY,sizeof(char*));
if (stack->buf == NULL){
return -1;
}
return 0;
}
void push(Stack* stack,char* str){
if (stack->top < stack->capacity){
stack->buf[++stack->top] = str;
}else{
int newSize = stack->capacity*2;
char **tmp = (char**)realloc(stack->buf, newSize*sizeof(char*));
if (tmp == NULL){
return;
}
stack->capacity = newSize;
stack->buf = tmp;
stack->buf[++stack->top] = str;
}
}
char *pop(Stack* stack){
return stack->buf[stack->top--];
}
boolean isEmpty(Stack* stack){
return stack->top == -1;
}
// 傳入的是二級指針
void destroy(Stack** stack){
free((*stack)->buf);
*stack = NULL;
}
創建測試代碼test.c
#include <stdio.h>
#include "stack.h"
int main(){
Stack s;
initStack(&s);
push(&s,"a");
push(&s,"b");
push(&s,"c");
push(&s,"d");
while (!isEmpty(&s)){
printf("%s\n",pop(&s));
}
Stack *p = &s;
destroy(&p);
return 0;
}
棧的經典運用
棧的一個經典案例是用來做四則混合運算的計算器算法,例如,編寫一個算法,解析字符串"6 + (8-3) * 2 + 10 / 5"
,並計算出該表達式的結果。如果使用詞法分析、語法分析的思路去處理,則不亞於開發一個編程語言的解析器,但是我們使用兩次棧就可以實現。首先將中綴表達式轉爲後綴表達式,然後再使用棧計算後綴表達式即可。
所謂中綴表達式,即我們通常所寫的算式,如:"6 + (8-3) * 2 + 10 / 5"
而後綴表達式則爲:"6 8 3 - 2 * + 10 5 / + "
,計算機很難處理中綴表達式,一旦轉爲後綴表達式,計算機處理起來就非常容易了。後綴表達式又稱爲逆波蘭表達式,相關知識可自行查詢。
總的來說,中綴表達式轉後綴表達式的規則是:遇數字就輸出,運算符進棧,左右括號匹配上一起出棧,棧頂要比較優先級,優先級低的出棧。所謂優先級,即乘除的優先級高於加減運算
以下源碼是筆者自行實現的一套算法,代碼已儘可能簡化,主要實現了整數的四則混合運算。如要包含浮點數,只需很小的改動即可。
首先將我們的棧結構改造一下,讓它支持泛型類型,關於C語言泛型處理,參照之前章節的內容。
頭文件stack.h
#ifndef _STACK_H_
#define _STACK_H_
#define DEFAULT_CAPACITY 10
#include <stdbool.h>
typedef struct
{
int top;
int capacity;
int typeSize; //數據類型的字節數
void *buf;
} Stack;
// 初始化棧
int ST_init(Stack*,int typeSize);
// 壓入棧
void ST_push(Stack*,void*);
// 彈出棧
void* ST_pop(Stack*);
// 判空
bool ST_isEmpty(Stack*);
// 查看棧頂
void* ST_top(Stack*);
// 銷燬棧
void ST_destroy(Stack*);
#endif
源文件stack.c
#include <stdlib.h>
#include <string.h>
#include "stack.h"
int ST_init(Stack* stack,int typeSize){
if (stack == NULL){
return -1;
}
stack->capacity = DEFAULT_CAPACITY;
stack->top = -1;
stack->typeSize = typeSize;
stack->buf = calloc(DEFAULT_CAPACITY,typeSize);
if (stack->buf == NULL){
return -1;
}
return 0;
}
void ST_push(Stack* stack,void* e){
if (stack->top < stack->capacity){
memcpy(stack->buf + (++stack->top) * stack->typeSize, e, stack->typeSize);
}else{
int newSize = stack->capacity*2;
char *tmp = (char*)realloc(stack->buf, newSize*sizeof(char*));
if (tmp == NULL){
return;
}
stack->capacity = newSize;
stack->buf = tmp;
memcpy(stack->buf + (++stack->top) * stack->typeSize, e, stack->typeSize);
}
}
void* ST_pop(Stack* stack){
return stack->buf + (stack->top--)*stack->typeSize;
}
bool ST_isEmpty(Stack* stack){
return stack->top == -1;
}
void* ST_top(Stack* stack){
if (stack->top == -1) return 0;
else return stack->buf + stack->top * stack->typeSize;
}
void ST_destroy(Stack* stack){
free(stack->buf);
}
編寫四則混合運算的源碼 calculator.c
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include "stack.h"
#define BUF_SIZE 256
#define ST_top_ch(st) *((char *)ST_top(st))
#define ST_pop_ch(st) *((char *)ST_pop(st))
#define ST_pop_int(st) *((int*)ST_pop(st))
// 將字符打印到數組
static void printChar(char ch,char *buf,int *offset){
if (*offset < BUF_SIZE)
*offset += sprintf(buf + (*offset),"%c",ch);
}
// 將空格作爲分割符,分割字符串,返回字符串數組
static char **splitStr(char *str){
char **arr = (char**)calloc(BUF_SIZE,sizeof(char*));
for (int i = 0; i < BUF_SIZE; i++){
arr[i] = (char*)calloc(50,sizeof(char));
if (arr[i] == NULL) return NULL;
if (i == 0) strcpy(arr[i],strtok(str, " "));
else{
char *p = strtok(NULL, " ");
if (p == NULL) {
free(arr[i]);
arr[i] = NULL;
return arr;
}
strcpy(arr[i],p);
}
}
}
// 掃描數字字符,打印到數組
static bool outDigit(char ch,char *buf,int *offset){
if (isspace(ch)) return true;
bool r = isdigit(ch);
if (!r) ch = ' ';
printChar(ch,buf,offset);
return r;
}
// 解析中綴表達式,並轉化爲後綴表達式
char **parseExpr(char *s,Stack *stack){
char str[BUF_SIZE]={0};
int offset = 0;
for (; *s !='\0'; s++){
while (outDigit(*s,str,&offset)) s++;
switch (*s){
case '+':
case '-':
while (!ST_isEmpty(stack) && ST_top_ch(stack) != '('){
printChar(ST_pop_ch(stack),str,&offset);
printChar(' ',str,&offset);
}
ST_push(stack,s);
break;
case '*':
case '/':
case '(':
ST_push(stack,s);
break;
case ')':
while (!ST_isEmpty(stack)){
char *ch = (char*)ST_pop(stack);
if (*ch == '(') break;
printChar(*ch,str,&offset);
printChar(' ',str,&offset);
}
break;
case '\0': // 使用goto語句跳出循環
goto end;
default:
printf("\nIllegal expression!!!\n");
break;
}
}
end:
while (!ST_isEmpty(stack)){
printChar(ST_pop_ch(stack),str,&offset);
printChar(' ',str,&offset);
}
printChar('\0',str,&offset);
return splitStr(str);
}
// 運算處理
static int operate(int a,int b,char op){
int res = 0;
switch (op){
case '+':
res = a + b;
break;
case '-':
res = a - b;
break;
case '*':
res = a*b;
break;
case '/':
res = a/b;
break;
default:
break;
}
return res;
}
// 計算後綴表達式
int calculate(Stack *stack,char **strings){
int num = 0 ,res=0;
for (int i = 0; i < BUF_SIZE; i++){
if (strings[i] == NULL) break;
if (isdigit(strings[i][0])){
num = atoi(strings[i]);
ST_push(stack,&num);
}else{
int a = ST_pop_int(stack);
int b = ST_pop_int(stack);
res = operate(b,a,strings[i][0]);
ST_push(stack,&res);
}
}
return ST_pop_int(stack);
}
int main(){
// 待計算的四則運算表達式
char *expression = "6 + (8-3) * 2 + 10 / 5";
Stack stk;
ST_init(&stk,sizeof(char));
// 將中綴表達式轉後綴表達式,並存入字符串數組中返回
char **strArr = parseExpr(expression,&stk);
Stack calcStk;
ST_init(&calcStk,sizeof(int));
// 計算後綴表達式
int result = calculate(&calcStk,strArr);
printf("result=%d\n",result);
// 釋放堆內存
for (int i = 0; i < BUF_SIZE; i++){
if (strArr[i] != NULL) free(strArr[i]);
}
free(strArr);
ST_destroy(&stk);
ST_destroy(&calcStk);
}
以上代碼使用GCC編譯器進行編譯:gcc calculator.c stack.c -o calculator
計算結果打印:
result=18
以上代碼中,需要注意的是strtok
字符串分割函數的使用,其他函數在前面的章節中都有涉及,不再說明,關於該函數的具體用法,請點擊查看博主的另一篇 博客