大家好,這里是Dark FalmeMater。
這篇文章我將超級(jí)仔細(xì)地講解快速排序,快排之所以叫快排,到底有多快,為什么這么快,還有快速排序的優(yōu)化和改進(jìn),通過(guò)這篇文章你一定會(huì)對(duì)快排有進(jìn)一步的掌握。
快排的歷史及介紹
快速排序由C. A. R. Hoare在1962年提出。
它的基本思想是:通過(guò)一趟排序?qū)⒁判虻臄?shù)據(jù)分割成獨(dú)立的兩部分,其中一部分的所有數(shù)據(jù)都比另外一部分的所有數(shù)據(jù)都要小,然后再按此方法對(duì)這兩部分?jǐn)?shù)據(jù)分別進(jìn)行快速排序,整個(gè)排序過(guò)程可以遞歸 進(jìn)行,以此達(dá)到整個(gè)數(shù)據(jù)變成有序序列。
?其中Hoare大佬寫的版本由于沒有那么容易理解且容易出現(xiàn)錯(cuò)誤,在后人的智慧下,進(jìn)行了小小的改變,分化出了挖坑法和雙指針法。我們逐個(gè)進(jìn)行講解。
Hoare版
遞歸函數(shù)分析思路如下
在數(shù)組中選定一個(gè)關(guān)鍵的點(diǎn),最右邊或者最右邊都可以。
?我們選擇最左邊進(jìn)行講解,找到關(guān)鍵位置后設(shè)置left和rght然后分別從數(shù)組左邊和最右邊開始遍歷,右邊先行,找到小于key位置的值就停下了,左邊的找大于key位置的值,找到后就互換,直到左下標(biāo)和右下標(biāo)相遇,相遇的時(shí)候說(shuō)明左邊已經(jīng)沒有大于key位置的值了,右邊沒有小于key位置的值了,交換相遇位置的值和key位置的值,就可以將該組數(shù)分割成以key位置的值分割的兩組數(shù)。
有了思路我們可以寫出以下的代碼:
//hoare版本
int PartSort1(int* a, int left, int right)
{
int key = a[left];//設(shè)置關(guān)鍵點(diǎn)
while (left < right)
{
while (a[right] > a[keyi])//從最右邊開始找到大于關(guān)鍵點(diǎn)的坐標(biāo)
{
--right;
}
//找大
while (a[left] < a[keyi])//從左邊開始找到小于key的坐標(biāo)
{
++left;
}
Swap(&a[left], &a[right]);//交換
}
Swap(key, &a[left]);//當(dāng)left相遇,就交換關(guān)鍵點(diǎn)和相遇點(diǎn)的值
return left;返回相遇點(diǎn)的坐標(biāo)
}
Hoare版解答疑惑及易錯(cuò)點(diǎn)指出:
- key的選擇
?之所以選擇最左邊的位置設(shè)置key,是因?yàn)槔眠@個(gè)值在寫代碼和進(jìn)行判斷時(shí)更方便一些,利用right位置的值當(dāng)做key當(dāng)然也可以,如果用遞歸函數(shù)傳參數(shù)組的第二個(gè)元素做key,在遞歸到每個(gè)小區(qū)間只有一個(gè)數(shù)時(shí)就會(huì)出現(xiàn)錯(cuò)誤,因?yàn)闆]有key。
- 相遇節(jié)點(diǎn)為什么可以和key位置的互換呢?為什么相遇點(diǎn)的數(shù)值一定會(huì)比key位置的值小呢?
大家可以思考一下…
這就要看左右指針誰(shuí)先行了,如動(dòng)圖所示
如果是right先行,會(huì)發(fā)生什么呢?其實(shí)除了最后left和right相遇,其他的都是一樣,如果是right先行的話。
在循環(huán)判斷中右邊先行是一定要注意的。
還有嗎?
還有呢。
判斷條件怎么寫呢?判斷條件是大于交換還是大于等于(相對(duì)left而言)交換呢?如果是left位置的值大于key的話,就交換有可能會(huì)發(fā)生森么情況呢?
?看這個(gè)圖,在交換了組邊的6和右邊的2之后,right開始–,因?yàn)榕袛鄺l件是a[right]>key,所以當(dāng)right找到5時(shí),5=key,right就跳出了循環(huán)。同理,因?yàn)椴粷M足left下標(biāo)的值小于key,left跳出循環(huán),交換以left為下標(biāo)和以right為下標(biāo)的值,交換的還是5和5,再次循環(huán),判斷l(xiāng)eft<right成立,繼續(xù)循環(huán),left和right還在原來(lái)的位置,繼續(xù)交換,就會(huì)陷入死循環(huán),所以這里的判斷條件要寫為a[left]>=key,a[right]<=key,然后再進(jìn)行交換。
看動(dòng)圖
判斷條件也要注意,這種情況很難想到,所以有時(shí)運(yùn)行錯(cuò)誤不知道問題出在哪里
更改上邊問題,將判斷條件改為a[right] >= key和a[left] <= a[keyi]
?思考了這么多,終于找到相遇的點(diǎn)后交換key和a[left](a[left]都可以),就可以將這個(gè)數(shù)組分為兩個(gè)大小不同的的區(qū)間啦!
真的嗎?
?前邊初始化key=a[left],交換key和遍歷后的a[left],然而a[0]并沒有和找到的值進(jìn)行交換,而是key這個(gè)數(shù)和找到的相遇點(diǎn)交換,這個(gè)時(shí)候a[left]就沒有改變?yōu)橄嘤鳇c(diǎn)找到的小于key的值,如果一直遞歸下去,會(huì)變成什么樣呢?(上邊動(dòng)圖部分,key改成a[left])
再來(lái)看排序該組數(shù)據(jù)
如果是創(chuàng)建的key和找到的點(diǎn)進(jìn)行交換,就會(huì)出現(xiàn)數(shù)據(jù)污染,好多數(shù)據(jù)都被更改了,這樣的排序一定是錯(cuò)誤的,所以初始化條件可以這樣。
int keyi=left;
int key=a[keyi]
在判斷時(shí)利用key,在交換時(shí)交換a[keyi]和a[left]即可。
終于結(jié)束啦,然而并沒有。
?要注意的是在right和left找的過(guò)程中l(wèi)eft也不能小于right,比如該數(shù)組是有序的,在排序的過(guò)程中,right先行,但是一直沒有找到小于a[keyi]的值,直到到達(dá)left的位置再次減一,就會(huì)越界訪問。
完整代碼如下
//hoare版本
int PartSort1(int* a, int left, int right)
{
//int midi=GetMidi(a,left,right)
//Swap(&a[left[,&a[midi]);
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
--right;
}
//找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
是不是細(xì)節(jié)滿滿,一不小心就會(huì)出來(lái)好多bug。
挖坑法
同樣先看圖
挖坑法相較于Hoare大佬的方法,步驟更加清晰明了一點(diǎn),一步一步實(shí)現(xiàn)
上邊的動(dòng)圖表現(xiàn)得十分清晰,設(shè)置一個(gè)坑位,保存left位置的值,設(shè)置hole變量等于left,然后從右邊找小于a[keyi]即上圖key的值,填入坑中,這時(shí)將找到的值的位置right設(shè)置為坑,然后從左往右找大于a[keyi]的值填入hole,將left所在位置設(shè)置為hole,不斷循環(huán),直到left等于right,將之前保存的key,即a[keyi]填進(jìn)坑里,就完成了前邊相同的操作。
代碼如下,很容易理解
//挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
//保存key值以后。左邊形成一個(gè)坑
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
--right;
}
a[hole] = a[right];
hole = right;
//左邊再走,找大,填到右邊的坑,左邊重新形成新的坑位
while (left < right && a[left] <= key)
{
++left;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
最后返回right,left,hole都可以,反正最后他們?cè)谕粋€(gè)位置上。
雙指針法
看幾遍動(dòng)圖,不管他們用什么方法進(jìn)行實(shí)現(xiàn),最終實(shí)現(xiàn)的效果都是相同的。雙指針法只要懂得了原理,還是很容易實(shí)現(xiàn)的。
雙指針法和前邊兩種方法略有不同,來(lái)體驗(yàn)一下。先看代碼后進(jìn)行講解
//前后指針法
int PartSort3(int* a, int left, int right)
{
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int prev = left;
int cur = prev + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
++prev;
if (prev != cur)
{
Swap(&a[prev], &a[cur]);
}
}
++cur;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
還是一樣選擇left位置的值作為key,設(shè)置兩個(gè)指針,一個(gè)指向數(shù)組最前邊的位置,一個(gè)在他的后邊,前后指針法因此得名。
具體思路:
?設(shè)置前后指針,后邊的指針只要不越界訪問,cur就一直往后走,如果prev的下一個(gè)位置不是cur,且cur找到小于key的值時(shí),就交換prev位置和cur位置的值。直到最后cur大于right結(jié)束循環(huán)。此時(shí)prev位置的值一定小于或等于key,因?yàn)閏ur在后邊查找的過(guò)程中最不濟(jì)的情況也就是一個(gè)也沒找到,prev和他自己換,照樣也不變,在找到第一個(gè)之前cur和prev一直一起移動(dòng)并一直在cur的后一位,當(dāng)cur找到大于key位置的坐標(biāo)后,prev不動(dòng),cur繼續(xù)移動(dòng),所以當(dāng)cur找到小于key的位置時(shí),prev的下一個(gè)位置一定是大于key的,可以放心交換。
直到最后cur走到數(shù)組盡頭,就如前邊所說(shuō),prev的下一位一定大于key,而prev位置的值一定小于等于key,交換a[prev]和a[keyi]的值,就可以將數(shù)組分成以prev位置為中心的兩組,左邊的值都小于等于key,右邊的值都大于key。
循環(huán)有一種更簡(jiǎn)單的方法表示,效果相同
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
&&操作符,當(dāng)前一條件為假,后一條件就不走,如果前一條件為真后,才會(huì)走后一條件,然后prev才會(huì)++,與上邊實(shí)現(xiàn)的效果相同。
遞歸函數(shù)
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort1(a, begin, end);
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
begin和end是傳進(jìn)來(lái)的數(shù)組段的第一個(gè)位置和最后一個(gè)位置的下標(biāo)。要注意接下來(lái)的遞歸數(shù)組的區(qū)間還有結(jié)束判斷條件。
舉一個(gè)例子說(shuō)明遞歸流程。
遞歸結(jié)束后,數(shù)組就變?yōu)橛行虻牧?。至于先往右遞歸還是先往左都可以,沒有區(qū)別。
上邊遞歸時(shí)用的Hoare大佬的版本,用挖坑法和雙指針法也都可以。
時(shí)間復(fù)雜度與空間復(fù)雜度
快排的時(shí)間復(fù)雜度為O(nlogN),空間復(fù)雜度為O(N),如果情況比較好的話為O(log2n),空間是可以復(fù)用的,一邊用完另一邊還可以用。
時(shí)間復(fù)雜度最壞的情況是若該數(shù)組為有序數(shù)組。最好情況就是一直取到中間位置。
最壞情況:
若數(shù)組數(shù)量為n,一次只排好了一個(gè)數(shù)據(jù),right第一次跑n次,第二次跑n-1次,則時(shí)間復(fù)雜度明顯為O(n^2)。
假使每次都能找到中間值,此時(shí)時(shí)間復(fù)雜度最低。2^h=n,所以n=log2 ^n。
最好情況時(shí)間復(fù)雜度為O(nlogn)。
至于空間復(fù)雜度從上邊的圖就可以看出,因?yàn)檫f歸開辟的空間是在棧上的,每次開辟的空間都可知小于n,故每次遞歸開辟空間為O(1),所以快排的時(shí)間復(fù)雜度為遞歸深度乘于O(1),最壞情況為O(N),最好的情況為O(logN)。
優(yōu)化
借助題目:排序數(shù)組
給一個(gè)數(shù)組,要求給他排序,要求很簡(jiǎn)單,卻只有50%的通過(guò)率,力扣標(biāo)記簡(jiǎn)單不一定簡(jiǎn)單,標(biāo)記中等那一定是有點(diǎn)難。
這題很顯然,普通的排序比如冒泡排序,插入排序,選擇排序是過(guò)不了的,我們剛剛學(xué)習(xí)了快排,何不嘗試一波。
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void QuickSort1(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort1(a, begin, end);
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
//hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
--right;
}
//找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
int* sortArray(int* nums, int numsSize, int* returnSize){
QuickSort1(nums, 0, numsSize-1);
*returnSize=numsSize;
return nums;
}
快排解題的代碼如下
然而卻發(fā)現(xiàn)
?用hoare大佬的方法走一遍,發(fā)現(xiàn)時(shí)間復(fù)雜度在這組用例上為O(N^2),right一直向右移,不會(huì)分成左右兩組,一直遞歸n次,right向前的次數(shù)從n到1,這樣的話花費(fèi)的時(shí)間太多了,針對(duì)這種情況進(jìn)行優(yōu)化,就要介紹三數(shù)取中。
三數(shù)取中
如何防止上面最壞的情況發(fā)生?
只需要防止最左邊的數(shù)為該組數(shù)里最小的數(shù)即可。
?可以找出left位置的值和right位置的值及(left+right)/2數(shù)組中間下標(biāo)的值,找出他們?nèi)齻€(gè)中間大小的一個(gè),與a[left]進(jìn)行交換,這樣就可以防止left是數(shù)組中最小的元素這種情況的發(fā)生。
將三數(shù)取中抽離成函數(shù)
//三數(shù)取中
int GetMidi(int* a, int left, int right)
{
int mid = (left + right) / 2;
//left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
}
在每次partsort前搞來(lái)中間值的坐標(biāo),與left進(jìn)行交換
int midi=GetMidi(a,left,right)
Swap(&a[left[,&a[midi]);
加上三數(shù)取中再次運(yùn)行
然而。。。。
?再次出乎意料,如果是全部數(shù)字都一樣或者有大量重復(fù)數(shù)據(jù),right還是像剛才有序數(shù)組的情況一樣,right一直向左,時(shí)間復(fù)雜度為O(N^2),此時(shí)三數(shù)取中也沒用了,無(wú)論怎么取,取到的都是2。針對(duì)這種情況怎么辦?
三路分化
將hoare大佬的方法和雙指針法結(jié)合起來(lái),將數(shù)組分為三個(gè)部分,左邊小于key,中間的區(qū)段是等于key的,右邊的部分是大于key的。
設(shè)置三個(gè)指針,兩個(gè)指針用于位置的交換是數(shù)組劃分為三個(gè)區(qū)間,一個(gè)指針用于遍歷。
看思路
最后在left和right之間的部分就是等于5的部分,left之前皆小于key,right之后皆大于key。
總結(jié)下來(lái)只有以下三種情況
- cur的值小于key,交換cur的值和left的值,cur++,left++。
- cur的值等于key,直接++cur
- cur的值大于key,交換cur和right的值,cur不能動(dòng)。
代碼實(shí)現(xiàn)
void QuickSort1(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;//記錄左右邊界
int end = right;
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < key)//重點(diǎn)理解
{
Swap(&a[cur], &a[left]);
++left;
++cur;
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right]);
}
else
{
++cur;
}
}
QuickSort1(a, begin, left-1);
QuickSort1(a, right+1, end);
}
這次我們信心滿滿,如果數(shù)組的元素全部為2的話,遍歷一遍,right和left直接相遇,甚至只需要O(N)的時(shí)間就可以完成。
更改代碼后運(yùn)行。
?還是超出時(shí)間限制,這又是為什么呢?這道題對(duì)快速排序做了針對(duì)性的處理,普通的快排很難過(guò)這道題,他會(huì)檢測(cè)我們的三數(shù)取中,然后搞出對(duì)應(yīng)的一串?dāng)?shù)字,讓每次取出的數(shù)都是數(shù)組中倒數(shù)第二大,這樣的話時(shí)間復(fù)雜度還是很大。
解決方法:
?選取數(shù)組中任意位置的數(shù)和left位置和right作比較選出keyi,而不是一直取中間坐標(biāo)與他們相比較,用rand函數(shù)進(jìn)行操作,使我們選數(shù)位置沒有規(guī)律可循,這樣編譯器就不能根據(jù)我們所寫的代碼搞出針對(duì)性用例。
代碼如下:
int GetMidi(int* a, int left, int right)
{
//int mid = (left + right) / 2;
int mid =left+(rand()%(right-left));
//left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void QuickSort1(int* a, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;//記錄左右邊界
int end = right;
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[cur], &a[left]);
++left;
++cur;
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right]);
--right;
}
else
{
++cur;
}
}
QuickSort1(a, begin, left-1);
QuickSort1(a, right+1, end);
}
int* sortArray(int* nums, int numsSize, int* returnSize){
srand(time(NULL));
QuickSort1(nums, 0, numsSize-1);
*returnSize=numsSize;
return nums;
}
運(yùn)行后如圖
可以發(fā)現(xiàn)所用時(shí)間還是很長(zhǎng),這道題如果用堆排序和shell排序都是很好過(guò)的,放在這里是為了提升我們對(duì)快排的理解和掌握。
小區(qū)間優(yōu)化
?在上邊顯示遞歸流程的圖中,我們可以看到遞歸的過(guò)程,類比樹的結(jié)構(gòu),樹的每一個(gè)節(jié)點(diǎn)都是一次遞歸,樹的最后一排的節(jié)點(diǎn)個(gè)數(shù)占全部節(jié)點(diǎn)的50%,倒數(shù)第二行占總個(gè)數(shù)的25%,如果分成的數(shù)組的量的變小到一定的個(gè)數(shù)的時(shí)候可以不再使用遞歸,而是選擇其他的方法進(jìn)行排序,可以減少大量的遞歸次數(shù)。
這種方法叫做小區(qū)間優(yōu)化,使用什么排序比較好呢?冒泡排序和選擇排序的時(shí)間復(fù)雜度為O(N2),選擇排序的時(shí)間復(fù)雜度最高為O(N2),可以使用選擇排序進(jìn)行小區(qū)間優(yōu)化,從而減少大量的遞歸次數(shù)
插入排序及小區(qū)間優(yōu)化后的代碼如下
//插入排序
void InsertSort(int* a, int n)
{
//[0,end]有序,把end+1的位置插入到前序序列
//控制[0,end+1]有序
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
}
else
{
break;
}-+
--end;
}
a[end + 1] = tmp;
}
}
//加上小區(qū)間優(yōu)化
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if ((end - begin + 1) > 10)//如果一個(gè)數(shù)組的元素小于10,就進(jìn)行插入排序
{
int keyi = PartSort2(a, begin, end);
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}
}
//加上小區(qū)間優(yōu)化
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if ((end - begin + 1) > 10)
{
int keyi = PartSort2(a, begin, end);
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
else
{
InsertSort(a + begin, end - begin + 1);
}
}
快排非遞歸版
復(fù)習(xí)一下:它的基本思想是:通過(guò)一趟排序?qū)⒁判虻臄?shù)據(jù)分割成獨(dú)立的兩部分,其中一部分的所有數(shù)據(jù)都比另外一部分的所有數(shù)據(jù)都要小,然后再按此方法對(duì)這兩部分?jǐn)?shù)據(jù)分別進(jìn)行快速排序,整個(gè)排序過(guò)程可以遞歸 進(jìn)行,以此達(dá)到整個(gè)數(shù)據(jù)變成有序序列。
將一整個(gè)數(shù)組不斷細(xì)分,在不斷劃分的過(guò)程中進(jìn)行有序化,最終完成排序的功能。在前邊partsort函數(shù)中,我們只要通過(guò)某種方法得到分成一組一組的小數(shù)組段的left和right即可。
上邊的說(shuō)明遞歸的流程圖是直接全部展開的,實(shí)際上是一直向左劃分,直到分出的數(shù)組left和right相等,然后回到上步。
如圖所示
結(jié)合代碼就很容易明白。
在非遞歸版本我們想和遞歸的步驟相同,如何得到上述遞歸過(guò)程中的left和right?這是問題之所在,而且在遞歸過(guò)程中向partsort1傳入不同的left和right。
可以借助棧先進(jìn)先出的功能來(lái)實(shí)現(xiàn),push起始的left和right,進(jìn)行第一次partsort,知道了left和right后,將他們?cè)賞op掉。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-715933.html
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort1(a, left, right);
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
快排講到這里就結(jié)束啦,如果你有耐心看完的話你一定會(huì)對(duì)快排的掌握有所提升噠。
加油!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-715933.html
到了這里,關(guān)于快排&超詳細(xì),Leetcode排序數(shù)組題目帶你升華掌握的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!