iOS重做輪子,寫一個NSDictionary(一)

從排序說起

一種很棒的排序算法,木桶排序(計數排序)。

算法如下,比如[ 1 9 3 7]需要排序。按木桶排序,我們要準備好10個桶,比如我們建立一個數組a[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0,} 0代表沒使用。
我們依次掃描 1 9 3 7 ,
遇到1 ,賦值a[1] = 1 // 1代表這個桶使用了
遇到9 ,賦值a[9] = 1
遇到3,賦值a[3] = 1
遇到7 ,賦值a[7] = 1
那么就排好了,我們再建立一個數組b,通過掃一遍a數組,遇到數組元素里面的值是1的,就把a數組的下標放到b數組里。最后的b數組就是這樣{1, 3, 7, 9}

我們知道,需要比較的排序,最快的時間復雜度是O(NlogN)。木桶很快,可以做到O(N)。但是也不是完美的,他的空間復雜度取決于最大的那個元素。當然實際使用我們不可能用這么多桶,畢竟內存不是無限的。這時候我們就想,能不能把下標的范圍縮小?例如我們只有5個桶,我要把1 9 3 7 都放進這五個桶里。那么我們只要把每個數字對 5 求模,就能實現我們的想法。最大的9,求模后得4。這就是NSDictionary的基本思想。

HashTable

NSDictionary是基于HashTable實現的一種稱為 key-value 的數據結構。跟桶排序一樣,hash利用一個叫hash的函數把key約束在0 ~ tablesize - 1 范圍內。這里我們不打算一開始談論HashTable的各種優缺點,我們的目的是寫一個簡單的,具有NSDictionary類似功能的輪子。在實現我們需求過程中去深入了解HashTable的各種特點。我們的字典只支持存儲字符串,沒有線程安全的考慮,但是實現實現這樣的輪子可以讓我們對hashtable這種數據結構了然于胸,對于字典我們不在感覺到神秘,實際上,很輕易的也能讓我們的字典支持泛型。

準備材料
我們給這個輪子起個名字叫YesDictionary
類似NSDictionary,我們需要做幾個決定:
1 容量選擇
2 桶數量的選擇
3 hash函數的選擇
4 填充因子
5 沖突解決辦法

容量選擇

在決定給我們的YesDictionary安排多少個桶的時候,我們先去 CFDictionary找點啟發。
在早期的CF文件中,Dictionary有如下定義

struct __CFDictionary {
    CFRuntimeBase _base;
    CFIndex _count;        /* number of values */
    CFIndex _capacity;        /* maximum number of values */
    CFIndex _bucketsNum;    /* number of slots */
    uintptr_t _marker;
    void *_context;        /* private */
    CFIndex _deletes;
    CFOptionFlags _xflags;      /* bits for GC */
    const void **_keys;        /* can be NULL if not allocated yet */
    const void **_values;    /* can be NULL if not allocated yet */
};

在新的CF文件中,apple 新加入了叫CFBasicHashXX的相關基礎類,因為很多數據結構都使用到hash,例如Set,Dictionary等。
在CFDictionary類的定義中,我們看到了_capacity,_bucketsNum,容量和桶的數量。apple給這兩個變量定義了取值范圍

static const uint32_t __CFDictionaryCapacities[42] = {
    4, 8, 17, 29, 47, 76, 123, 199, 322, 521, 843, 1364, 2207, 3571, 5778, 9349,
    15127, 24476, 39603, 64079, 103682, 167761, 271443, 439204, 710647, 1149851, 1860498,
    3010349, 4870847, 7881196, 12752043, 20633239, 33385282, 54018521, 87403803, 141422324,
    228826127, 370248451, 599074578, 969323029, 1568397607, 2537720636U
};

static const uint32_t __CFDictionaryBuckets[42] = {    // primes
    5, 11, 23, 41, 67, 113, 199, 317, 521, 839, 1361, 2207, 3571, 5779, 9349, 15121,
    24473, 39607, 64081, 103681, 167759, 271429, 439199, 710641, 1149857, 1860503, 3010349,
    4870843, 7881193, 12752029, 20633237, 33385273, 54018521, 87403763, 141422317, 228826121,
    370248451, 599074561, 969323023, 1568397599, 2537720629U, 4106118251U
};

上面的意思是,如果字典容量是4,就準備5個桶,如果容量是8,就準備11個桶,很顯然桶的數量必須比容量要大。這就叫字典的擴充。仔細看我們還能發現一些規律,就是桶數量都是素數。為了能把key放在正確的桶里,我們需要對key進行hash,或者叫壓縮,取特征,怎樣都行。《算法導論》建議桶的數量取素數比較保險。取素數的目的,是為了把一系列的key hash后求模,他們的值能均勻分布在0 ~ tablesize - 1 的范圍里,減少沖突(不同的key 產生同樣的hash值稱為沖突,畢竟一個桶不能放兩個key)。至于其中的數學論證,有興趣的可以自行了解。
因此,我們的 YesDictionary 使用桶的數量也取素數。

目前解決只是桶的數量,還有容量,這個跟填充因子有關聯,我們一起討論(呼,離著手編碼還有一段距離呢)。填充因子其實就是容量和桶的數量的比值。填充因子低了,桶的利用率就低了,填充因子高了,發生沖突的概率就大大的提升,效率就變低。大部分hashmap 的填充因子選擇了0.75 ,至于為什么不是0.6,0.5,這就涉及到hash碰撞中,節點出現的頻率在hash桶中遵循泊松分布,也就是說填充因子超過0.75,沖突的幾率就大大增加。所以我們的填充因子也選擇0.75。

先給出我們山寨版的YesDictionary的結構定義

typedef signed int YesIndex;
typedef enum {
    YES_EMPTY = 0,
    YES_FULL,
    YES_DEL
} YesStatus;


typedef struct _YesDictionary {
    YesIndex count; //字典鍵值對的個數
    YesIndex capacityLevel; //容量等級,擴充的時候用到
    YesIndex capacity; //字典容量
    YesIndex bucketsNum; //字典桶的個數
    YesStatus *marker;  // fixit 標記每個桶的狀態
    CFStringRef *keys; //字典的keys
    CFStringRef *values; //字典values
} YesDictionary, *YesDictionaryRef;

我們用YesStatus表示桶的狀態,它具有空桶,桶被填充,桶的數據被刪除三種狀態。
CFDictionary _marker變量,看上去像個整型,其實它是一個指針,地址是 0xa1b1c1d3,無法得知這個地址指向哪里,但是蘋果用這個對桶的狀態進行了標記,我們YesDictionary開辟了一個marker數組進行標記。這稍微多占用了一些內存。

下面是我們打算使用的桶

static int _capacity[MAX_LEVEL] = {
    4, 8, 17
};
static int _bukets[MAX_LEVEL] = {
    5, 11, 23
};

MAX_LEVEL 定義為3 ,意味著YesDictionary 只支持兩次擴容,也就是我們的桶最多只有23個,后面可以任意添加。目前僅作說明使用。

我們先看怎么創建一個YesDictionary字典

YesDictionaryRef YesDictionaryCreate(void) {
    YesDictionaryRef dic = malloc(sizeof(YesDictionary));
    memset(dic, 0, sizeof(YesDictionary));
    return _YesDictionaryInit(dic);
}

這段代碼我們申請了一小塊malloc控制的內存,并且用memset把這段內存清零。然后使用_YesDictionaryInit函數初始化YesDictionary。

YesDictionaryRef _YesDictionaryInit(YesDictionaryRef dic) {
    dic->capacityLevel = 0;
    dic->capacity = _capacity[dic->capacityLevel];
    dic->bucketsNum = _bukets[dic->capacityLevel];
    dic->count = 0;
    size_t size = sizeof(CFStringRef);
    YesIndex num = dic->bucketsNum;
    CFStringRef* keys = calloc(num, size);
    CFStringRef* values = calloc(num, size);
    YesStatus* marker = calloc(num, sizeof(YesStatus));
    dic->keys = keys;
    dic->values = values;
    dic->marker = marker;
    return dic;
}

_YesDictionaryInit 給字典進行初始化,分配了keys values marker所需要的內存。這里用了calloc函數,這個函數支持將分配的內存清零。

接著,實現字典的鍵值對添加

void YesDictionaryAdd(YesDictionaryRef dic, CFStringRef key, CFStringRef value) {
    
    float factor = dic->count * 1.0f / dic->capacity;
    if (factor > FILL_FACTOR) {
        _YesDictionaryGrow(dic);
    }
    
    YesIndex match;
     YesStatus status = _findBucketIndex(dic, key, &match);
    if (match != kCFNotFound) {
        if (YES_FULL == status) {
            CFRelease(dic->keys[match]);
            CFRelease(dic->values[match]);
        } else {
            ++dic->count;
        }
        dic->marker[match] = YES_FULL;
        dic->keys[match] = key;
        dic->values[match] = value;
        CFRetain(key);
        CFRetain(value);
    }
}

在添加前,我們根據填充因子決定字典是否需要擴容。如果需要,就執行擴容方法_YesDictionaryGrow,否則,調用_findBucketIndex函數尋找一個可以使用的桶。添加鍵值對,我們需要對其進行一次retain,防止釋放,同時把桶的狀態標記為YES_FULL。

_YesDictionaryGrow 實現了字典的擴容

void _YesDictionaryGrow(YesDictionaryRef dic) {
    if (dic->capacityLevel == MAX_LEVEL - 1) {
        printf("小demo,不能增長了\n");
        return;
    }

    YesStatus* oldMarker = dic->marker;
    CFStringRef* oldKeys = dic->keys;
    CFStringRef* oldValues = dic->values;
    YesIndex oldBucketsNum = _bukets[dic->capacityLevel];
    
    ++ dic->capacityLevel;
    dic->capacity = _capacity[dic->capacityLevel];
    dic->bucketsNum = _bukets[dic->capacityLevel];
    size_t size = sizeof(CFStringRef);
    YesIndex num = dic->bucketsNum;
    CFStringRef* keys = calloc(num, size);
    CFStringRef* values = calloc(num, size);
    YesStatus* marker = calloc(num, sizeof(YesStatus));
    dic->marker = marker;
    dic->keys = keys;
    dic->values = values;
    
    for (YesIndex i = 0; i < oldBucketsNum; ++i) {
        if (oldMarker[i] != YES_FULL) continue;
        YesIndex match;
        _findBuketIndex(dic, oldKeys[i], &match);
        marker[match] = YES_FULL;
        keys[match] = oldKeys[i];
        values[match] = oldValues[i];
    }

    free(oldMarker);
    free(oldKeys);
    free(oldValues);
}

這個函數邏輯很簡單,通過遞增capacityLevel 為字典選擇下一級的桶,然后把就舊桶上的key-value 通過_findBuketIndex 映射到新的桶里。需要注意的是,這里不需要對 key-value retain操作,并且記得釋放 舊桶 和舊標記。

_findBuketIndex函數是整個字典的關鍵,它函數指示了我們的 key-value 要存放到那個桶里

YesStatus _findBuketIndex(YesDictionaryRef dic, const CFStringRef key, YesIndex* found) {
    *found = kCFNotFound;
    YesIndex probe = _hash(key) % dic->bucketsNum;
    YesStatus *marker = dic->marker;
    CFStringRef *keys = dic->keys;
    YesIndex probeStep = 1;
    while (true) {
        YesStatus status = marker[probe];
        switch (status) {
            case YES_EMPTY: {
                *found = probe;
                return YES_EMPTY;
            }
            case YES_FULL: {
                if (kCFCompareEqualTo == CFStringCompare(key, keys[probe], kCFCompareCaseInsensitive)) {
                    *found = probe;
                    return YES_FULL;
                }
                probe += probeStep;
                break;
            }
            case YES_DEL: {
                *found = probe;
                return YES_DEL;
            }
        }
        if (probe >= dic->bucketsNum) {
            probe -= dic->bucketsNum;
        }
    }
}

YesIndex probe = _hash(key) % dic->bucketsNum; 這句代碼計算了key 的hash值。其中關鍵是hash函數的實現,在apple公開的文件中,CFDictionary 的hash函數筆者找不到其實現。請注意的是,CFDictionary有一個hash函數如下

CF_PRIVATE CFHashCode __CFBasicHashHash(CFTypeRef cf) {
    CFBasicHashRef ht = (CFBasicHashRef)cf;
    return CFBasicHashGetCount(ht);
}

這個函數簡單的返回字典鍵值對的數量,它并不是對key的hash,而是字典本身的的hash。因為假如這是對key的hash,那么插入和查找的時候,hash的結果會不一致。
以下是YesDictionary 的hash函數,這個叫time33,大部分hashtable都使用這個函數,他是簡單不斷的乘以33而得名。time33簡單,并且能取得很好的分布效果。同時我們給hash一個初值 MAGIC_INIT,值為5381,據說能獲得更好的分布效果。

uint32_t _hash(CFStringRef str) {
    char buffer[BUFSIZE];
    CFStringEncoding encoding = kCFStringEncodingUTF8;
    const char *ptr = CFStringGetCStringPtr(str, encoding);
    if (ptr == NULL) {
        if (CFStringGetCString(str, buffer, BUFSIZE, encoding)) ptr = buffer;
    }
    return time33(ptr);
}

uint32_t time33(const char *str) {
    uint32_t hash = MAGIC_INIT;
    while (*str) {
        hash += (hash << 5 ) + (*(str++));
    }
    return (hash & 0x7FFFFFFF);
}

_findBuketIndex同時處理了key相同的情況, YesDictionary 的key只能唯一,所以我們簡單的將上一個key-value覆蓋。如果大量的key hash后產生的桶的位置一樣,這種沖突現象稱為一次堆積,或叫一次聚集。 _findBuketIndex解決沖突使用了開放定址法,簡單的將hash值 + 1 ,循環尋找下一個桶,直到找到為止。這種方法比拉鏈法簡單,有效。它不像拉鏈法需要同時維護鏈表這種數據結構,同時,簡單的地址偏移,實現成本很低。除此之外,還有再散列等,再散列有可能造成二次堆積,或叫二次聚集。。。有關解決沖突的方法,有興趣的可以自行深入,這里就不再討論了。

到目前為止,YesDictionary 插入實現了。為了方便觀察,我們給YesDictionary實現 show 和 relaese 方法。

void YesDictionaryShow(YesDictionaryRef dic) {
    CFStringRef *keys = dic->keys;
    CFStringRef *values = dic->values;
    YesStatus *marker = dic->marker;
    CFMutableStringRef showString = CFStringCreateMutable(kCFAllocatorDefault, 1024);
    for (YesIndex i = 0, count = dic->bucketsNum; i != count; ++i) {
        if (marker[i] != YES_FULL) continue;
        CFStringAppend(showString, keys[i]);
        CFStringAppendCString(showString, " = ", kCFStringEncodingUTF8);
        CFStringAppend(showString, values[i]);
        CFStringAppendCString(showString, "\n", kCFStringEncodingUTF8);
    }
    CFShow(showString);
    CFRelease(showString);
}

void YesDictionaryRelease(YesDictionaryRef dic) {
    CFStringRef *keys = dic->keys;
    CFStringRef *values = dic->values;
    YesStatus *marker = dic->marker;
    for (YesIndex i = 0, count = dic->bucketsNum; i != count; ++i) {
        if (marker[i] != YES_FULL) continue;
        CFRelease(keys[i]);
        CFRelease(values[i]);
    }
    free(marker);
    free(keys);
    free(values);
    free(dic);
}

YesDictionary 的其他刪除和查找方法和插入大同小異,這里就不一一實現了。接下來我們看看它能不能工作。

   YesDictionaryRef dic = YesDictionaryCreate();
    
    char key[50], value[50];
    for (int i = 0; i < 13; ++i) {
        sprintf(key, "%d", i);
        sprintf(value, "value(%d)", i);
        CFStringRef cfkey = CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8);
        CFStringRef cfvalue = CFStringCreateWithCString(kCFAllocatorDefault, value, kCFStringEncodingUTF8);
        YesDictionaryAdd(dic, cfkey, cfvalue);
        CFRelease(cfkey);
        CFRelease(cfvalue);
    }
    
    YesDictionaryShow(dic);
    YesDictionaryRelease(dic);

我們創建一個字典dic,并且循環插入0~12做為key,value 就是 value(i)。要注意的是不要插入超過17個元素,因為YesDictionary只有最多23個桶。
在控制臺輸出如下


pic.png

因為我們是有序輸入,所以key分布效果不會太好。不管怎么樣,我們的YesDictionary can work 了。

下一篇文章《iOS重做輪子,寫一個NSDictionary(二)》,我們繼續重做輪子,用紅黑樹實現一個NSDictionary。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • HashMap 是 Java 面試必考的知識點,面試官從這個小知識點就可以了解我們對 Java 基礎的掌握程度。網...
    野狗子嗷嗷嗷閱讀 6,705評論 9 107
  • Java集合類可用于存儲數量不等的對象,并可以實現常用的數據結構如棧,隊列等,Java集合還可以用于保存具有映射關...
    小徐andorid閱讀 1,974評論 0 13
  • Map 是一種很常見的數據結構,用于存儲一些無序的鍵值對。在主流的編程語言中,默認就自帶它的實現。C、C++ 中的...
    一縷殤流化隱半邊冰霜閱讀 9,317評論 23 67
  • 我知道 掏空棉絮的熊布偶 獰笑著 默許了折疊刀的詛咒 我知道 豁開的,順著嘴角延伸的裂口 想要愈合,還要等些時候 ...
    彌九的詩閱讀 351評論 8 2
  • 新的一年又開始了,一定要加倍努力,賺很多的小錢錢。今年幾個項目都要展開,我相信我們一定能做的很好!
    淡盡相思閱讀 130評論 0 0