程序員C語言快速上手——高級篇(十一)

高級篇

數據結構

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);
    }
}

基於數組和基於鏈式的線性表各有特點,這裏做一個線性表小結

  1. 基於數組的線性表,添加、刪除元素性能較差,根據以上代碼可知,當頻繁添加或刪除元素時,需要底層動態數組不斷的申請或移動內存,而頻繁申請堆內存是非常耗費性能的,但是基於數組的線性表,其具有數組的快速索引特點,查詢定位元素時速度非常快
  2. 基於鏈式的線性表,其查詢元素時較爲耗費性能,且與查詢的元素所處的位置相關。當鏈表元素越多時,被查詢的元素越靠後,其速度越慢。這是因爲鏈表的查詢必須從頭節點開始遍歷,如果被查詢的元素正好是最後一個,那麼就需要將前面所有節點遍歷一次。相對的,鏈表添加、刪除元素的性能非常高,因爲它不需要移動內存,只需要將指針指向一個新的元素。

鏈表的經典運用

先來看一道算法題:

給出兩個 非空 的鏈表用來表示兩個非負的整數。其中,它們各自的位數是按照 逆序 的方式存儲的,並且它們的每個節點只能存儲 一位 數字。
如果,我們將這兩個數相加起來,則會返回一個新的鏈表來表示它們的和。
您可以假設除了數字 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字符串分割函數的使用,其他函數在前面的章節中都有涉及,不再說明,關於該函數的具體用法,請點擊查看博主的另一篇 博客

歡迎關注我的公衆號:編程之路從0到1

編程之路從0到1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章