IOS排序算法之桶排序、計數(shù)排序、基數(shù)排序

桶排序、計數(shù)排序、基數(shù)排序和前面講的那些排序有所不同,不是基于比較的排序算法,而是一種線性排序。他們的時間復雜度更低:O(n),但是對要排序的數(shù)據(jù)要求很苛刻,所以我們今天學習重點的是掌握這些排序算法的適用場景

桶排序

首先,我們來看桶排序。桶排序,顧名思義,會用到“桶”,核心思想是將要排序的數(shù)據(jù)分到幾個有序的桶里,每個桶里的數(shù)據(jù)再單獨進行排序。桶內(nèi)排完序之后,再把每個桶里的數(shù)據(jù)按照順序依次取出,組成的序列就是有序的了。

image.png

桶排序的時間復雜度為什么是O(n)呢?我們一塊兒來分析一.下。

如果要排序的數(shù)據(jù)有n個,我們把它們均勻地劃分到m個桶內(nèi),每個桶里就有k=n/m個元素。每個桶內(nèi)部使用快速排序,時間復雜度為O(k * logk)。m個桶排序的時間復雜度就是O(m*k logk),因為k=n/m,所以整個桶排序的時間復雜度就是0(nlog(n/m))。當桶的個數(shù)m接近數(shù)據(jù)個數(shù)n時,log(n/m) 就是一個非常小的常量,這個時候桶排序的時間復雜度接近0(n)。

桶排序看起來很優(yōu)秀,那它是不是可以替代我們之前講的排序算法呢?

答案當然是否定的。為了讓你輕松理解桶排序的核心思想,我剛才做了很多假設。實際上,桶排序?qū)σ判驍?shù)據(jù)的要求是非常苛刻的。

首先,要排序的數(shù)據(jù)需要很容易就能劃分成m個桶,并且,桶與桶之間有著天然的大小順序。這樣每個桶內(nèi)的數(shù)據(jù)都排序完之后,桶與桶之間的數(shù)據(jù)不需要再進行排序。

其次,數(shù)據(jù)在各個桶之間的分布是比較均勻的。如果數(shù)據(jù)經(jīng)過桶的劃分之后,有些桶里的數(shù)據(jù)非常多,有些非常少,很不平均,那桶內(nèi)數(shù)據(jù)排序的時間復雜度就不是常量級了。在極端情況下,如果數(shù)據(jù)都被劃分到一個桶里,那就退化為O(nlogn)的排序算法了。

桶排序比較適合用在外部排序中

所謂的外部排序就是數(shù)據(jù)存儲在外部磁盤中,數(shù)據(jù)量比較大,內(nèi)存有限,無法將數(shù)據(jù)全部加載到內(nèi)存中。

比如說我們有10GB的訂單數(shù)據(jù),我們希望按訂單金額(假設金額都是正整數(shù))進行排序,但是我們的內(nèi)存有限,只有幾百MB,沒辦法- - -次性把10GB的數(shù)據(jù)都加載到內(nèi)存中。這個時候該怎么辦呢?

現(xiàn)在我來講一下,如何借助桶排序的處理思想來解決這個問題。

我們可以先掃描一遍文件,看訂單金額所處的數(shù)據(jù)范圍。假設經(jīng)過掃描之后我們得到,訂單金額最小是1元,最大是10萬元。我們將所有訂單根據(jù)金額劃分到100個桶里,第一個桶我們存儲金額在1元到1000元之內(nèi)的訂單,第二桶存儲金額在1001元到2000元之內(nèi)的訂單,以此類推。每一個桶對應一個文件,并且按照金額范圍的大小順序編號命名(00, 01, 02...99) 。

理想的情況下,如果訂單金額在1到10萬之間均勻分布,那訂單會被均勻劃分到100個文件中,每個小文件中存儲大約100MB的訂單數(shù)據(jù),我們就可以將這100個小文件依次放到內(nèi)存中,用快排來排序。等所有文件都排好序之后,我們只需要按照文件編號,從小到大依次讀取每個小文件中的訂單數(shù)據(jù),并將其寫入到一個文件中,那這個文件中存儲的就是按照金額從小到大排序的訂單數(shù)
據(jù)了。

不過,你可能也發(fā)現(xiàn)了,訂單按照金額在1元到10萬元之間并不一-定是均勻分布的,所以10GB訂單數(shù)據(jù)是無法均勻地被劃分到100個文件中的。有可能某個金額區(qū)間的數(shù)據(jù)特別多,劃分之后對應的文件就會很大,沒法一次性讀入內(nèi)存。這又該怎么辦呢?

針對這些劃分之后還是比較大的文件,我們可以繼續(xù)劃分,比如,訂單金額在1元到1000元之間的比較多,我們就將這個區(qū)間繼續(xù)劃分為10個小區(qū)間,1元到100元,101 元到200元,201 元到300元...901元到1000元。如果劃分之后,101 元到200元之間的訂單還是太多,無法- -次性讀入內(nèi)存,那就繼續(xù)再劃分,直到所有的文件都能讀入內(nèi)存為止。

代碼如下:

#pragma mark -
#pragma mark 桶排序
- (void)bucketSort:(NSMutableArray *)datasource
{
    //預計每個桶內(nèi)能裝3個
    NSInteger size = 3;
    
    //桶的數(shù)量
    NSInteger bucketsCount = datasource.count / size;
    
    //找出最小值和最大值
    NSInteger min = [datasource[0] integerValue];
    NSInteger max = [datasource[0] integerValue];
    
    for (NSNumber *number in datasource)
    {
        if (number.integerValue < min)
        {
            min = number.integerValue;
        }
        
        if (number.integerValue > max)
        {
            max = number.integerValue;
        }
    }
    
    //平均值
    NSInteger average = ceil((double)(max - min)/(double)bucketsCount);
    
    NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
    
    for (NSInteger i = 0; i < bucketsCount; i++)
    {
        NSMutableArray *bucketArray = [NSMutableArray array];
        NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)];
        [dictionary setValue:bucketArray forKey:key];
    }
    
    for (NSNumber *number in datasource)
    {
        NSInteger i = floor((double)(number.integerValue - min) / (double)average);
        NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)];
        NSMutableArray *bucketArray = [dictionary valueForKey:key];
        [bucketArray addObject:number];
    }
    
    NSInteger length = 0;
    for (NSInteger i = 0; i < dictionary.allKeys.count; i++)
    {
        NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)];
        NSMutableArray *bucketArray = [dictionary objectForKey:key];
        [self quickSort:bucketArray];
        [datasource replaceObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(length, bucketArray.count)] withObjects:bucketArray];
        length += bucketArray.count;
    }
}

計數(shù)排序

我個人覺得,計數(shù)排序其實是桶排序的-種特殊情況。當要排序的n個數(shù)據(jù),所處的范圍并不大的時候,比如最大值是k,我們就可以把數(shù)據(jù)劃分成k個桶。每個桶內(nèi)的數(shù)據(jù)值都是相同的,省掉了桶內(nèi)排序的時間。

我們都經(jīng)歷過高考,高考查分數(shù)系統(tǒng)你還記得嗎?我們查分數(shù)的時候,系統(tǒng)會顯示我們的成績以及所在省的排名。如果你所在的省有50萬考生,如何通過成績快速排序得出名次呢?

考生的滿分是900分,最小是0分,這個數(shù)據(jù)的范圍很小,所以我們可以分成901個桶,對應分數(shù)從0分到900分。根據(jù)考生的成績,我們將這50萬考生劃分到這901個桶里。桶內(nèi)的數(shù)據(jù)都是分數(shù)相同的考生,所以并不需要再進行排序。我們只需要依次掃描每個桶,將桶內(nèi)的考生依次輸出到一個數(shù)組中,就實現(xiàn)了50萬考生的排序。因為只涉及掃描遍歷操作,所以時間復雜度是O(n)。

計數(shù)排序的算法思想就是這么簡單,跟桶排序非常類似,只是桶的大小粒度不一樣。不過,為什么這個排序算法叫“計數(shù)”排序呢?“計數(shù)”的含義來自哪里呢?

想弄明白這個問題,我們就要來看計數(shù)排序算法的實現(xiàn)方法。我還拿考生那個例子來解釋。為了方便說明,我對數(shù)據(jù)規(guī)模做了簡化。假設只有8個考生,分數(shù)在0到5分之間。這8個考生的成績我們放在一個數(shù)組A[8]中,它們分別是: 2, 5, 3, O, 2, 3, O, 3。

考生的成績從0到5分,我們使用大小為6的數(shù)組C[6]表示桶,其中下標對應分數(shù)。不過,C[6]內(nèi)存儲的并不是考生,而是對應的考生個數(shù)。像我剛剛舉的那個例子,我們只需要遍歷一遍考生分數(shù),就可以得到C[6]的值。

image.png

從圖中可以看出,分數(shù)為 3 分的考生有 3 個,小于 3 分的考生有 4 個,所以,成績?yōu)?3 分的考生在排序之后的有序數(shù)組 R[8] 中,會保存下標 4,5,6 的位置。

image.png

那我們?nèi)绾慰焖儆嬎愠觯總€分數(shù)的考生在有序數(shù)組中對應的存儲位置呢?這個處理方法非常巧
妙,很不容易想到。

思路是這樣的:我們對C[6]數(shù)組順序求和,C[6] 存儲的數(shù)據(jù)就變成了下面這樣子。C[k] 里存儲小
于等于分數(shù)k的考生個數(shù)。

image.png

有了前面的數(shù)據(jù)準備之后,現(xiàn)在我就要講計數(shù)排序中最復雜、最難理解的一部分了,請集中精力跟著我的思路!

我們從后到前依次掃描數(shù)組A。比如,當掃描到3時,我們可以從數(shù)組C中取出下標為3的值7,也就是說,到目前為止,包括自己在內(nèi),分數(shù)小于等于3的考生有7個,也就是說3是數(shù)組R中的第7個元素(也就是數(shù)組R中下標為6的位置)。當3放入到數(shù)組R中后,小于等于3的元素就只剩下了6個了,所以相應的C[3]要減1,變成6。

以此類推,當我們掃描到第2個分數(shù)為3的考生的時候,就會把它放入數(shù)組R中的第6個元素的位置(也就是下標為5的位置)。當我們掃描完整個數(shù)組A后,數(shù)組R內(nèi)的數(shù)據(jù)就是按照分數(shù)從小到大有序排列的了。

image.png

代碼如下:

#pragma mark -
#pragma mark 計數(shù)排序
- (void)countingSort:(NSMutableArray *)a
{
    if (a.count <= 1)
    {
        return;
    }
    
    NSInteger max = [a[0] integerValue];
    
    for (NSNumber *number in a)
    {
        if (number.integerValue > max)
        {
            max = number.integerValue;
        }
    }
    
    NSMutableArray *c = [NSMutableArray array];
    
    for (NSInteger i = 0; i <= max; i++)
    {
        [c addObject:@(0)];
    }
    
    NSMutableArray *r = [NSMutableArray array];
    for (NSInteger i = 0; i < a.count; i++)
    {
        [r addObject:@(0)];
        
        NSNumber *index = a[i];
        NSNumber *count = c[index.integerValue];
        [c replaceObjectAtIndex:index.integerValue withObject:@(count.integerValue + 1)];
    }
    
    for (NSInteger i = 1; i <= max; i++)
    {
        [c replaceObjectAtIndex:i withObject:@([c[i] integerValue] + [c[i - 1] integerValue])];
    }
    
    for (NSInteger i = a.count - 1; i >= 0; i--)
    {
        NSNumber *index = a[i];
        NSInteger count = [c[index.integerValue] integerValue] - 1;
        
        [r replaceObjectAtIndex:count withObject:a[i]];
        [c replaceObjectAtIndex:index.integerValue withObject:@(count)];
    }
    
    for (NSInteger i = 0; i < a.count; i++)
    {
        [a replaceObjectAtIndex:i withObject:r[i]];
    }
}

這種利用另外一個數(shù)組來計數(shù)的實現(xiàn)方式是不是很巧妙呢?這也是為什么這種排序算法叫計數(shù)排序的原因。不過,你千萬不要死記硬背上面的排序過程,重要的是理解和會用。

我總結(jié)一下,計數(shù)排序只能用在數(shù)據(jù)范圍不大的場景中,如果數(shù)據(jù)范圍k比要排序的數(shù)據(jù)n大很多,就不適合用計數(shù)排序了。而且,計數(shù)排序只能給非負整數(shù)排序,如果要排序的數(shù)據(jù)是其他類型的,要將其在不改變相對大小的情況下,轉(zhuǎn)化為非負整數(shù)。

比如,還是拿考生這個例子。如果考生成績精確到小數(shù)后- -位, 我們就需要將所有的分數(shù)都先乘以10,轉(zhuǎn)化成整數(shù),然后再放到9010個桶內(nèi)。再比如,如果要排序的數(shù)據(jù)中有負數(shù),數(shù)據(jù)的范圍是[-1000, 1000],那我們就需要先對每個數(shù)據(jù)都加1000,轉(zhuǎn)化成非負整數(shù)。

基數(shù)排序

我們再來看這樣一個排序問題。假設我們有10萬個手機號碼,希望將這10萬個手機號碼從小到大排序,你有什么比較快速的排序方法呢?

我們之前講的快排,時間復雜度可以做到O(nlogn),還有更高效的排序算法嗎?桶排序、計數(shù)排序能派上用場嗎?手機號碼有11位,范圍太大,顯然不適合用這兩種排序算法。針對這個排序問題,有沒有時間復雜度是O(n)的算法呢?現(xiàn)在我就來介紹一種新的排序算法,基數(shù)排序。

剛剛這個問題里有這樣的規(guī)律:假設要比較兩個手機號碼a, b的大小,如果在前面幾位中,a手機號碼已經(jīng)比b手機號碼大了,那后面的幾位就不用看了。

借助穩(wěn)定排序算法,這里有一個巧妙的實現(xiàn)思路。還記得我們第11節(jié)中,在闡述排序算法的穩(wěn)定性的時候舉的訂單的例子嗎?我們這里也可以借助相同的處理思路,先按照最后一位來排序手機號碼,然后,再按照倒數(shù)第二位重新排序,以此類推,最后按照第一位重新排序。經(jīng)過11次排序之后,手機號碼就都有序了。

手機號碼稍微有點長,畫圖比較不容易看清楚,我用字符串排序的例子,畫了一張基數(shù)排序的過程分解圖,你可以看下。

image.png

注意,這里按照每位來排序的排序算法要是穩(wěn)定的,否則這個實現(xiàn)思路就是不正確的。因為如果是非穩(wěn)定排序算法,那最后一次排序只會考慮最高位的大小順序,完全不管其他位的大小關(guān)系,那么低位的排序就完全沒有意義了。

根據(jù)每一位來排序,我們可以用剛講過的桶排序或者計數(shù)排序,它們的時間復雜度可以做到O(n)。如果要排序的數(shù)據(jù)有k位,那我們就需要k次桶排序或者計數(shù)排序,總的時間復雜度是O(k*n)。當k不大的時候,比如手機號碼排序的例子,k最大就是11,所以基數(shù)排序的時間復雜度就近似于O(n)。

實際上,有時候要排序的數(shù)據(jù)并不都是等長的,比如我們排序牛津字典中的20萬個英文單詞,最短的只有1個字母,最長的我特意去查了下,有45個字母,中文翻譯是塵肺病。對于這種不等長的數(shù)據(jù),基數(shù)排序還適用嗎?

實際上,我們可以把所有的單詞補齊到相同長度,位數(shù)不夠的可以在后面補“0”,因為根據(jù)ASClI值,所有字母都大于“O”,所以補“0”不會影響到原有的大小順序。這樣就可以繼續(xù)用基數(shù)排序了。

我來總結(jié)一下,基數(shù)排序?qū)σ判虻臄?shù)據(jù)是有要求的,需要可以分割出獨立的“位”來比較,而且位之間有遞進的關(guān)系,如果a數(shù)據(jù)的高位比b數(shù)據(jù)大,那剩下的低位就不用比較了。除此之外,每一位的數(shù)據(jù)范圍不能太大,要可以用線性排序算法來排序,否則,基數(shù)排序的時間復雜度就無法做到O(n)了。

代碼如下:

#pragma mark -
#pragma mark 基數(shù)排序
- (void)radixSort:(NSMutableArray *)datasource
{
    NSMutableArray *bucket = [self createBucket];
    NSInteger maxNumber = [self listMaxItem:datasource];
    NSInteger maxLength = [self numberLength:maxNumber];
    
    for (NSInteger digit = 1; digit <= maxLength; digit++)
    {
        //入桶
        for (NSNumber *number in datasource)
        {
            NSInteger baseNumber = [self fetchBaseNumber:number.integerValue digit:digit];
            NSMutableArray *subArray = bucket[baseNumber];
            [subArray addObject:number];
        }
        
        //出桶
        NSInteger index = 0;
        
        for (NSInteger i = 0; i < bucket.count; i++)
        {
            NSMutableArray *subArray = bucket[i];
            while (subArray.count > 0)
            {
                NSNumber *item = subArray[0];
                [datasource replaceObjectAtIndex:index withObject:item];
                [subArray removeObjectAtIndex:0];
                index++;
            }
        }
    }
}

//創(chuàng)建10個空桶
- (NSMutableArray *)createBucket
{
    NSMutableArray *bucketArray = [NSMutableArray array];
    
    for (NSInteger i = 0; i < 10; i++)
    {
        [bucketArray addObject:[NSMutableArray array]];
    }
    
    return bucketArray;
}

//計算無序序列中最大的數(shù)值
- (NSInteger)listMaxItem:(NSArray *)array
{
    NSInteger maxNumber = [array[0] integerValue];
    
    for (NSNumber *number in array)
    {
        if (maxNumber < number.integerValue)
        {
            maxNumber = number.integerValue;
        }
    }
    
    return maxNumber;
}

//獲取數(shù)字的長度
- (NSInteger)numberLength:(NSInteger)number
{
    NSString *numberStr = [NSString stringWithFormat:@"%ld",(long)number];
    
    return numberStr.length;
}

//獲取數(shù)值中特定位數(shù)的值
- (NSInteger)fetchBaseNumber:(NSInteger)number digit:(NSInteger)digit
{
    if (digit > 0 && digit <= [self numberLength:number])
    {
        NSMutableArray *numbersArray = [NSMutableArray array];
        
        NSString *numberStr = [NSString stringWithFormat:@"%ld",(long)number];
    
        for (NSInteger i = 0; i < numberStr.length; i++)
        {
            NSString *subStr = [numberStr substringWithRange:NSMakeRange(i, 1)];
            [numbersArray addObject:[NSNumber numberWithInteger:subStr.integerValue]];
        }
        
        return [numbersArray[numbersArray.count - digit] integerValue];
    }
    
    return 0;
}

內(nèi)容小結(jié)

今天,我們學習了3種線性時間復雜度的排序算法,有桶排序、計數(shù)排序、基數(shù)排序。它們對要排序的數(shù)據(jù)都有比較苛刻的要求,應用不是非常廣泛。但是如果數(shù)據(jù)特征比較符合這些排序算法的要求,應用這些算法,會非常高效,線性時間復雜度可以達到0(n)。

桶排序和計數(shù)排序的排序思想是非常相似的,都是針對范圍不大的數(shù)據(jù),將數(shù)據(jù)劃分成不同的桶來實現(xiàn)排序。基數(shù)排序要求數(shù)據(jù)可以劃分成高低位,位之間有遞進關(guān)系。比較兩個數(shù),我們只需要比較高位,高位相同的再比較低位。而且每一-位的數(shù)據(jù)范圍不能太大,因為基數(shù)排序算法需要借助桶排序或者計數(shù)排序來完成每- -個位的排序工作。

最后:

自己寫了一個NSMutableArray+GLYSort算法分類,只需1行代碼,即可完成復雜排序操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內(nèi)容