笨辦法學C 練習33:鏈表算法

練習33:鏈表算法

原文:Exercise 33: Linked List Algorithmshtml

譯者:飛龍git

我將想你介紹涉及到排序的兩個算法,你能夠用它們操做鏈表。我首先要警告你,若是你打算對數據排序,不要使用鏈表,它們對於排序十分麻煩,而且有更好的數據結構做爲替代。我向你介紹這兩種算法只是由於它們難以在鏈表上完成,而且讓你思考如何高效操做它們。github

爲了編寫這本書,我打算將算法放在兩個不一樣的文件中,list_algos.hlist_algos.c,以後在list_algos_test.c中編寫測試。如今你要按照個人結構,由於它足以把事情作好,可是若是你使用其它的庫要記住這並非通用的結構。算法

這個練習中我打算給你一些額外的挑戰,而且但願你不要做弊。我打算先給你單元測試,而且讓你打下來。以後讓你基於它們在維基百科中的描述,嘗試實現這個兩個算法,以後看看你的代碼是否和個人相似。編程

冒泡排序和歸併排序

互聯網的強大之處,就是我能夠僅僅給你冒泡排序歸併排序的連接,來讓你學習它們。是的,這省了我不少字。如今我要告訴你如何使用它們的僞代碼來實現它們。你能夠像這樣來實現算法:數據結構

  • 閱讀描述,而且觀察任何可視化的圖表。函數

  • 使用方框和線條在紙上畫出算法,或者使用一些帶有數字的卡片(好比撲克牌),嘗試手動執行算法。這會向你形象地展現算法的執行過程。性能

  • list_algos.c文案總建立函數的主幹,而且建立list_algos.h文件,以後建立測試代碼。單元測試

  • 編寫第一個測試而且編譯全部東西。學習

  • 回到維基百科頁面,複製粘貼僞代碼到你建立的函數中(不是C代碼)。

  • 將僞代碼翻譯成良好的C代碼,就像我教你的那樣,使用你的單元測試來保證它有效。

  • 爲邊界狀況補充一些測試,例如空鏈表,排序號的鏈表,以及其它。

  • 對下一個算法重複這些過程並測試。

我只是告訴你理解大多數算法的祕密,直到你碰到一些更加麻煩的算法。這裏你只是按照維基百科來實現冒泡排序和歸併排序,它們是一個好的起始。

單元測試

下面是你應該經過的單元測試:

#include "minunit.h"
#include <lcthw/list_algos.h>
#include <assert.h>
#include <string.h>

char *values[] = {"XXXX", "1234", "abcd", "xjvef", "NDSS"};
#define NUM_VALUES 5

List *create_words()
{
    int i = 0;
    List *words = List_create();

    for(i = 0; i < NUM_VALUES; i++) {
        List_push(words, values[i]);
    }

    return words;
}

int is_sorted(List *words)
{
    LIST_FOREACH(words, first, next, cur) {
        if(cur->next && strcmp(cur->value, cur->next->value) > 0) {
            debug("%s %s", (char *)cur->value, (char *)cur->next->value);
            return 0;
        }
    }

    return 1;
}

char *test_bubble_sort()
{
    List *words = create_words();

    // should work on a list that needs sorting
    int rc = List_bubble_sort(words, (List_compare)strcmp);
    mu_assert(rc == 0, "Bubble sort failed.");
    mu_assert(is_sorted(words), "Words are not sorted after bubble sort.");

    // should work on an already sorted list
    rc = List_bubble_sort(words, (List_compare)strcmp);
    mu_assert(rc == 0, "Bubble sort of already sorted failed.");
    mu_assert(is_sorted(words), "Words should be sort if already bubble sorted.");

    List_destroy(words);

    // should work on an empty list
    words = List_create(words);
    rc = List_bubble_sort(words, (List_compare)strcmp);
    mu_assert(rc == 0, "Bubble sort failed on empty list.");
    mu_assert(is_sorted(words), "Words should be sorted if empty.");

    List_destroy(words);

    return NULL;
}

char *test_merge_sort()
{
    List *words = create_words();

    // should work on a list that needs sorting
    List *res = List_merge_sort(words, (List_compare)strcmp);
    mu_assert(is_sorted(res), "Words are not sorted after merge sort.");

    List *res2 = List_merge_sort(res, (List_compare)strcmp);
    mu_assert(is_sorted(res), "Should still be sorted after merge sort.");
    List_destroy(res2);
    List_destroy(res);

    List_destroy(words);
    return NULL;
}


char *all_tests()
{
    mu_suite_start();

    mu_run_test(test_bubble_sort);
    mu_run_test(test_merge_sort);

    return NULL;
}

RUN_TESTS(all_tests);

建議你從冒泡排序開始,使它正確,以後再測試歸併。我所作的就是編寫函數原型和主幹,讓這三個文件可以編譯,但不能經過測試。以後你將實現填充進入以後纔可以工做。

實現

你做弊了嗎?以後的練習中,我只會給你單元測試,而且讓本身實現它。對於你來講,不看這段代碼知道你本身實現它是一種很好的練習。下面是list_algos.clist_algos.h的代碼:

#ifndef lcthw_List_algos_h
#define lcthw_List_algos_h

#include <lcthw/list.h>

typedef int (*List_compare)(const void *a, const void *b);

int List_bubble_sort(List *list, List_compare cmp);

List *List_merge_sort(List *list, List_compare cmp);

#endif
#include <lcthw/list_algos.h>
#include <lcthw/dbg.h>

inline void ListNode_swap(ListNode *a, ListNode *b)
{
    void *temp = a->value;
    a->value = b->value;
    b->value = temp;
}

int List_bubble_sort(List *list, List_compare cmp)
{
    int sorted = 1;

    if(List_count(list) <= 1) {
        return 0;  // already sorted
    }

    do {
        sorted = 1;
        LIST_FOREACH(list, first, next, cur) {
            if(cur->next) {
                if(cmp(cur->value, cur->next->value) > 0) {
                    ListNode_swap(cur, cur->next);
                    sorted = 0;
                }
            }
        }
    } while(!sorted);

    return 0;
}

inline List *List_merge(List *left, List *right, List_compare cmp)
{
    List *result = List_create();
    void *val = NULL;

    while(List_count(left) > 0 || List_count(right) > 0) {
        if(List_count(left) > 0 && List_count(right) > 0) {
            if(cmp(List_first(left), List_first(right)) <= 0) {
                val = List_shift(left);
            } else {
                val = List_shift(right);
            }

            List_push(result, val);
        } else if(List_count(left) > 0) {
            val = List_shift(left);
            List_push(result, val);
        } else if(List_count(right) > 0) {
            val = List_shift(right);
            List_push(result, val);
        }
    }

    return result;
}

List *List_merge_sort(List *list, List_compare cmp)
{
    if(List_count(list) <= 1) {
        return list;
    }

    List *left = List_create();
    List *right = List_create();
    int middle = List_count(list) / 2;

    LIST_FOREACH(list, first, next, cur) {
        if(middle > 0) {
            List_push(left, cur->value);
        } else {
            List_push(right, cur->value);
        }

        middle--;
    }

    List *sort_left = List_merge_sort(left, cmp);
    List *sort_right = List_merge_sort(right, cmp);

    if(sort_left != left) List_destroy(left);
    if(sort_right != right) List_destroy(right);

    return List_merge(sort_left, sort_right, cmp);
}

冒泡排序並不難以理解,雖然它很是慢。歸併排序更爲複雜,實話講若是我想要犧牲可讀性的話,我會花一點時間來優化代碼。

歸併排序有另外一種「自底向上」的實現方式,可是它太難了,我就沒有選擇它。就像我剛纔說的那樣,在鏈表上編寫排序算法沒有什麼意思。你能夠把時間都花在使它更快,它比起其餘可排序的數據結構會至關版。鏈表的本質決定了若是你須要對數據進行排序,你就不要使用它們(尤爲是單向的)。

你會看到什麼

若是一切都正常工做,你會看到這些:

$ make clean all
rm -rf build src/lcthw/list.o src/lcthw/list_algos.o tests/list_algos_tests tests/list_tests
rm -f tests/tests.log 
find . -name "*.gc*" -exec rm {} \;
rm -rf `find . -name "*.dSYM" -print`
cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  -fPIC   -c -o src/lcthw/list.o src/lcthw/list.c
cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  -fPIC   -c -o src/lcthw/list_algos.o src/lcthw/list_algos.c
ar rcs build/liblcthw.a src/lcthw/list.o src/lcthw/list_algos.o
ranlib build/liblcthw.a
cc -shared -o build/liblcthw.so src/lcthw/list.o src/lcthw/list_algos.o
cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  build/liblcthw.a    tests/list_algos_tests.c   -o tests/list_algos_tests
cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  build/liblcthw.a    tests/list_tests.c   -o tests/list_tests
sh ./tests/runtests.sh
Running unit tests:
----
RUNNING: ./tests/list_algos_tests
ALL TESTS PASSED
Tests run: 2
tests/list_algos_tests PASS
----
RUNNING: ./tests/list_tests
ALL TESTS PASSED
Tests run: 6
tests/list_tests PASS
$

這個練習以後我就不會向你展現這樣的輸出了,除非有必要向你展現它的工做原理。你應該能知道我運行了測試,而且經過了全部測試。

如何改進

退回去查看算法描述,有一些方法可用於改進這些實現,其中一些是很顯然的:

  • 歸併排序作了大量的鏈表複製和建立操做,尋找減小它們的辦法。

  • 歸併排序的維基百科描述提到了一些優化,實現它們。

  • 你能使用List_splitList_join(若是你實現了的話)來改進歸併排序嘛?

  • 瀏覽全部防護性編程原則,檢查並提高這一實現的健壯性,避免NULL指針,而且建立一個可選的調試級別的不變量,在排序後實現is_sorted的功能。

附加題

  • 建立單元測試來比較這兩個算法的性能。你須要man 3 time來查詢基本的時間函數,而且須要運行足夠的迭代次數,至少以幾秒鐘做爲樣本。

  • 改變須要排序的鏈表中的數據總量,看看耗時如何變化。

  • 尋找方法來建立不一樣長度的隨機鏈表,而且測量須要多少時間,以後將它可視化並與算法的描述對比。

  • 嘗試解釋爲何對鏈表排序十分麻煩。

  • 實現List_insert_sorted(有序鏈表),它使用List_compare,接收一個值,將其插入到正確的位置,使鏈表有序。它與建立鏈表後再進行排序相比怎麼樣?

  • 嘗試實現維基百科上「自底向上」的歸併排序。上面的代碼已是C寫的了,因此很容易從新建立,可是要試着理解它的工做原理,並與這裏的低效版本對比。

相關文章
相關標籤/搜索