原文:Exercise 33: Linked List Algorithmshtml
譯者:飛龍git
我將想你介紹涉及到排序的兩個算法,你能夠用它們操做鏈表。我首先要警告你,若是你打算對數據排序,不要使用鏈表,它們對於排序十分麻煩,而且有更好的數據結構做爲替代。我向你介紹這兩種算法只是由於它們難以在鏈表上完成,而且讓你思考如何高效操做它們。github
爲了編寫這本書,我打算將算法放在兩個不一樣的文件中,list_algos.h
和list_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.c
和list_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_split
和List_join
(若是你實現了的話)來改進歸併排序嘛?
瀏覽全部防護性編程原則,檢查並提高這一實現的健壯性,避免NULL
指針,而且建立一個可選的調試級別的不變量,在排序後實現is_sorted
的功能。
建立單元測試來比較這兩個算法的性能。你須要man 3 time
來查詢基本的時間函數,而且須要運行足夠的迭代次數,至少以幾秒鐘做爲樣本。
改變須要排序的鏈表中的數據總量,看看耗時如何變化。
尋找方法來建立不一樣長度的隨機鏈表,而且測量須要多少時間,以後將它可視化並與算法的描述對比。
嘗試解釋爲何對鏈表排序十分麻煩。
實現List_insert_sorted
(有序鏈表),它使用List_compare
,接收一個值,將其插入到正確的位置,使鏈表有序。它與建立鏈表後再進行排序相比怎麼樣?
嘗試實現維基百科上「自底向上」的歸併排序。上面的代碼已是C寫的了,因此很容易從新建立,可是要試着理解它的工做原理,並與這裏的低效版本對比。