C語言標準庫 qsort bsearch 源碼實現

C語言是簡潔的強大的,固然也有不少坑。C語言也是有點業界良心的,至少它實現了2個最最經常使用的算法:快速排序和二分查找。算法

咱們知道,對於C語言標準庫 qsort和 bsearch:數組

a. 它是「泛型」的,能夠對任何類型進行排序或二分。函數

b. 咱們使用時必須自定義一個比較函數看成函數指針傳入。學習

c語言要實現泛型,基本上就只有 void指針提供的弱爆了的泛型機制,容易出錯。優化

這篇文章中,我實現了 標準庫qsort和bsearch函數,最基本的正確性和泛型固然要保證了。spa

在這裏,不涉及優化(寫標準庫實現的那幫人巴不得用匯編實現),只展示算法的運行原理和泛型的實現機制。指針

1.C語言標準庫qsort源碼實現。我先呈上完整實現,而後具體剖析。code

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

void swap(const void* a, const void* b, int size)
{
    assert(a != NULL && b != NULL);
    char tmp = 0;
    int i = 0;
    while (size > 0) {
        tmp = *((char*)a + i);
        *((char*)a + i) = *((char*)b + i);
        *((char*)b + i) = tmp;
        ++i;
        --size;
    }
}

void Qsort(void* base, int left, int right, int size, int (*cmp)(const void* a, const void* b))
{
    assert(base != NULL && size >= 1 && cmp != NULL);    /* left may be < 0 because of the last - 1 */
    if (left >= right) return;
    char* pleft = (char*)base + left * size;
    char* pkey = (char*)base + (left + (right - left) / 2) * size;
    swap(pleft, pkey, size);
    int last = left;
    char* plast = (char*)base + last * size;
    for (int i = left + 1; i <= right; ++i) {
        char* pi = (char*)base + i * size;
        if (cmp(pi, pleft) < 0) {
            ++last;
            plast = (char*)base + last * size;
            swap(pi, plast, size);
        }
    }
    swap(pleft, plast, size);
    Qsort(base, left, last - 1, size, cmp);
    Qsort(base, last + 1, right, size, cmp);
}

int cmp_string(const void* a, const void* b)
{
    assert(a != NULL && b != NULL);
    const char** lhs = (const char**)a;
    const char** rhs = (const char**)b;
    return strcmp(*lhs, *rhs);
}

int cmp_int(const void* a, const void* b)
{
    assert(a != NULL && b != NULL);
    const int* lhs = (const int*)a;
    const int* rhs = (const int*)b;
    if (*lhs < *rhs) {
        return -1;
    } else if (*lhs == *rhs) {
        return 0;
    } else {
        return 1;
    }
}

int main(int argc, char* argv[])
{
    int a[] = {-2, 0, 5, 1, 10, 8, 5, 4, 3, 9};
    int len1 = sizeof(a) / sizeof(a[0]);
    fprintf(stdout, "before sort:\n");
    for (int i = 0; i < len1; ++i) {
        fprintf(stdout, "%d ", a[i]);
    }
    fprintf(stdout, "\n");

    Qsort(a, 0, len1 - 1, sizeof(a[0]), cmp_int);
    fprintf(stdout, "after sort:\n");
    for (int i = 0; i < len1; ++i) {
        fprintf(stdout, "%d ", a[i]);
    }
    fprintf(stdout, "\n");


    const char* b[] = {"what", "chenwei", "skyline", "wel", "dmr"};
    int len2 = sizeof(b) / sizeof(b[0]);
    fprintf(stdout, "before sort:\n");
    for (int i = 0; i < len2; ++i) {
        fprintf(stdout, "%s-->", b[i]);
    }
    fprintf(stdout, "\n");

    Qsort(b, 0, len2 - 1, sizeof(b[0]), cmp_string);
    fprintf(stdout, "after sort:\n");
    for (int i = 0; i < len2; ++i) {
        fprintf(stdout, "%s-->", b[i]);
    }
    fprintf(stdout, "\n");
}

qsort基本實如今K&R裏有很是詳細的描述,我這裏重點解釋的是 swap函數,這個函數是實現泛型的基石。blog

首先對qsort函數原型說明幾點:排序

a.Qsort函數原型裏面沒有標準庫qsort的 len, 而是使用 left 和 right 來指明每次待排序的區間,用兩個值來表示一個區間很是直觀且優雅。

b.對於數據類型大小,我沒有使用 size_t 無符號類型。

C語言中無符號類型雖然能夠對數組提供負向越界保證和2倍空間,可是因爲坑爹的類型提高規則滋生了N多的bug,我是儘可能少用這個。

 

而後就是 swap函數.

首先咱們知道 數據元素都是以 比特位 形式暫存在 內存中的,後簡化爲以 字節 形式暫存在內存中。

咱們日常的一個 swap 函數,若是交換的數據是 int 型, 咱們就是:

void swap(int* a , int* b) {
     int tmp = *a;
     *a = *b;
     *b = tmp;
}

如今咱們不知道元素是什麼類型,那咱們怎麼作到泛型呢? 首先 傳入的兩個指針確定是 void 指針,如何肯定 void所指元素類型呢? 其實咱們這裏是不須要知道 元素類型的。

咱們這裏的目的是什麼? 咱們就是要 交換兩個元素的 字節序列,咱們每次交換一個字節,直到交換完爲止,這樣就達到目的了,這時咱們就須要第三個參數:待排元素的所佔字節數。

咱們知道C語言中結構體是能夠直接複製的,好比:

struct test {
    int a;
    char b;
};
struct test a, b;
a = b;

C語言是怎樣支持這種直接複製呢? 其實最終就是 兩個元素的字節序列的複製。注意到,若是struct裏面有指針,那麼這個結構體是引用語義的,兩個元素的指針成員指向同一內存。

咱們這裏的swap函數其實就是 手工模擬了 相似結構體複製 的整個過程。咱們每次交換一個 字節,總共交換的次數就是 元素的sizeof大小。

注意兩點:

a. 數據的字節序列是有 大端小端之分的,個人這個實現是不能 跨大端小端的。若是排序過程都始終都在同一機器中,那麼無需擔憂。

b. 數據的字節序列多是有 內存對齊的,主要是在結構體之中。 因此個人這個實現受 機器 內存對齊規則的影響,可是 排序發生在同一機器中的話,這個是不會影響正確性的。

總而言之,代碼實現受機器底層 數據大小端表示和 內存對齊策略 的影響。可是實際上 一個排序過程是不可能 一部分進行在這臺機,另外一部分在另一臺機器上進行的(僅考慮單機),因此個人這個實現是能夠很好工做的。

Qsort的快排思想就很簡單了,咱們最須要注意的就是 每次對 交換元素的首字節地址進行更新,咱們都是經由char*轉換,由於char*所指元素正好1字節,正好模擬每次一字節的swap.

 

2.C語言標準庫bsearch源碼實現

void* Bsearch(void* base, int len, int size, const void* key, int (*cmp)(const void* a, const void* b))
{
    assert(base != NULL && len >= 0 && size >= 1 && cmp != NULL);
    int low = 0;
    int high = len - 1; 
    while (low <= high) {
        int mid = low + (high - low) / 2;
        char* pmid = (char*)base + mid * size;
        if (cmp(pmid, key) < 0) {
            low = mid + 1;
        } else if (cmp(pmid, key) > 0) {
            high = mid - 1;
        } else {
            return pmid;
        }

    }
    return NULL;
}

int cmp_int(const void* a, const void* b)
{
    assert(a != NULL && b != NULL);
    const int* lhs = (const int*)a;
    const int* rhs = (const int*)b;
    if (*lhs < *rhs) {
        return -1;
    } else if (*lhs == *rhs) {
        return 0;
    } else {
        return 1;
    }
}

int cmp_string(const void* a, const void* b)
{
    assert(a != NULL && b != NULL);
    const char** lhs = (const char**)a;
    const char** rhs = (const char**)b;
    return strcmp(*lhs, *rhs);
}

int main(int argc, char* argv[])
{
    int a[] = {-2, 0, 1, 3, 4, 5, 5, 8, 9, 10};
    int len1 = sizeof(a) / sizeof(a[0]);

    int tmp = 5;
    int* res1 = (int*)Bsearch(a, len1, sizeof(a[0]), &tmp, cmp_int);
    if (res1 != NULL) {
        fprintf(stdout, "found it\n");
    } else {
        fprintf(stdout, "Not found\n");
    }

    const char* str[] = {"chenwei", "dmr", "skyline", "wel", "what"};
    int len2 = sizeof(str) / sizeof(str[0]);
    const char* p = "chenwei";
    char* res2 = (char*)Bsearch(str, len2, sizeof(str[0]), &p, cmp_string);
    if (res2 != NULL) {
        fprintf(stdout, "found it\n");
    } else {
        fprintf(stdout, "Not found\n");
    }
    return 0;
}

 歡迎你們批評指正,共同窗習。轉載請註明出處,謝謝。

相關文章
相關標籤/搜索