1. 程式人生 > >演算法導論第八章思考題

演算法導論第八章思考題

8-1(比較排序的概率下界) 在這一問題中,我們將證明對於給定的n個互異的輸入元素,任何確定或隨機的比較排序演算法,其概率執行時間都有下界Ω(nlgn)。首先分析一個確定的比較排序演算法A,其決策樹為Ta,假設A的輸入的每一種排列情況都是等可能的。

a) 假設Ta的每個葉子結點都標有在給定的隨機輸入情況下到達該結點的概率。證明:恰有n!個葉子結點標有1/n!,其他的葉結點標記為0.

因為對於n個元素有n!種可能陣列元素排列,那麼由此對應於決策樹的n!個葉子結點,對於n!個葉子結點中的每一個葉子結點對應一種排序情況,因為假設每一種排列情況都是等可能的,所以到達每一個葉子結點的概率1/n!,對於決策樹來說,我們只考慮這n!個葉子結點。其他葉子結點不屬於這n!種排序情況,所以隨機輸入排序情況到達該結點的概率是0.

b)定義D(T)表示一顆決策樹T的外部路徑長度,即D(T)是T的所有葉結點深度的和,假設T為一顆有k>1個葉結點的決策樹,LT和RT分別是T的左子樹和右子樹。證明:D(T)=D(LT)+D(RT)+k.

c)定義d(k)為所有具有k>1葉結點的決策樹T的最小D(T)值。證明:d(k)=min{d(i)+d(k-i)+k}.


d)證明:d對於給定的k(k>1)和i(1<i<k-1),函式ilgi+(k-i)lg(k-i)在i=k/2處取得最小值,並有結論d(k)=Ω(klgk).



e)證明:D(Ta)=Ωm(n!lgn!),並得出在平均情況下,排序n個元素的時間代價Ω(nlgn)


f) 不太懂。

8-2 (線性時間原址排序) 假設有一個包含n個待排序資料記錄的陣列,且每條記錄的關鍵字的值為0或1,對這樣一組記錄進行排序的演算法可能具備如下三種特性中的一部分。

1,演算法的時間代價是O(n).

2,演算法是穩定的。

3,演算法是原址排序,除了輸入陣列之外,演算法只需要固定的額外儲存空間。

a,給出滿足1和2的演算法

計數排序

b,給出滿足1和3的演算法

既然n個數只有0和1兩種值,那麼可以按照我設計的演算法,滿足線性時間,不需要輔助空間,但是不穩定。

#include <iostream>
#include <time.h>
using namespace std;
void _8_2_b(int A[],int n)
{
   int k=0;
   for (int i=0;i<n;i++)
   {
      if (A[i]==0)
      {
          k++;
      }
   }
   for (int j=0;j<n;j++)
   {
       if (j<=k)
       {
           A[j]=0;
       }
       else 
       {
           A[j]=1;
       }
    }
}
void main()
{
    const n=100;
    int A[n]={0};
    srand( (unsigned)time( NULL ) );
    for (int i=0;i<n;i++)
    {
        A[i]=rand()%2;
    }
    _8_2_b(A,n);
    for ( i=0;i<n;i++)
    {
        cout<<A[i]<<" ";
    }
}


c,給出滿足2和3的演算法

插入排序

d 你設計的演算法a-c中的任一個是否可以用於RADIX-SORT的第2行作為基礎排序方法,從而使RADIX-SORT在排序有b位關鍵字的n條記錄時的時間代價是O(bn)?如果可以,請解釋應如何處理,如果不行,請說明原因。

(a)可以。用計數排序作為基數排序的子程式即可。
(b)不可以。我設計的這個程式,只是在特定情況下可以排序,不具備普遍性。
(c)不可以。插入排序屬於比較排序演算法,所以其時間下界是Ω(nlgn)。沒有達到O(bn).

e.假設有n條記錄,其中所有關鍵字的值都在1到k的區間內,你應該如何修改計數排序,使得它可以在O(n+k)時間內完成對n條記錄的原址排序。除輸入陣列外,你可以O(k)使用大小的額外儲存空間。你給出的演算法是穩定的嗎?

不穩定。以下是具體演算法:

//此演算法也是對計數排序的一種改進,這裡有以下3點改進。
//1.只需要計算等於A[i]的元素,不用新增迴圈用來計算小於A[i]的元素
//2.同時也不用新增輔助陣列B來對陣列A進行輸出,只需要在原來的A陣列上進行排序就可以了。
//3.對於含有重複資料的陣列進行排序時,用於存放資料出現次數的陣列C的長度是刨去重複資料的最小值k,但是對於重複資料的處理是不穩定的。
//但是我還在思考這個排序是否是原址的計數排序?懂的請留言和我交流下哦,謝謝!
#include <iostream>
#include <time.h>
using namespace std;
const n=20;
void COUNTING_SORT(int A[n])
{
    int max=A[0],min=A[0];
    for (int i=0;i<n;i++)
    {
        if (A[i]>max)
        {
            max=A[i];
        }
        if (A[i]<min)
        {
            min=A[i];
        }
    }
    int k=max-min+1;   
    int *C=new int[k+1];
    for ( i=0;i<=k;i++)
    {
        C[i]=0;
    }
    for (int j=0;j<n;j++)
    {
        C[A[j]-min+1]++;
    }
    for (j=n-1;j>=0;)
    {
        k=max-min+1;
        if(C[k]!=0)
        {
          A[j]=max;
          C[k]--;j--;
        }
        else 
        {
            max--;
        }
    }
}
void main()
{
    int A[n]={0};
    srand( (unsigned)time( NULL ) );
    for (int i=0;i<n;i++)
    {
        A[i]=rand()%100;
    }
    COUNTING_SORT(A);
    for (int j=0;j<n;j++)
    {
        cout<<A[j]<<" ";
    }   
}

8-3 (變長資料項的排序)

a.給定一個整數陣列,其中不同的整數所包含的的數字的位數可能不同,但該陣列中,所有整數中包含的總數字位數為n.設計一個演算法,使其可以在O(n)時間內對該陣列進行排序。

#include <iostream>
#include <time.h>
using namespace std;
const n=15,m=4;//n代表元素個數最大值m代表位數最大值 
int Max(int B[n][m],int h)//O(n)
{//求最大k值
    int k=0;
    for (int i=0;i<n;i++)//O(n)
    {
        if (B[i][h]>k)
        {
            k=B[i][h];
        }
    }
    return k;
}
void COUNTING_SORT(int B[n][m],int C[n],int k,int h)//O(k)+O(n)+O(k)+O(n)=O(n+k)
{//計數排序
    
    int *D=new int[k+1];
    for (int i=0;i<=k;i++)//O(k)
    {
        D[i]=0;
    }
    for (int j=0;j<n;j++)//O(n)
    {
        D[B[j][h]]=D[B[j][h]]+1;
    }
    for (i=0;i<=k;i++)//O(k)
    {
        D[i+1]=D[i+1]+D[i];
    }
    for (j=n-1;j>=0;j--)//O(n)
    {
        C[D[B[j][h]]-1]=j;
        D[B[j][h]]=D[B[j][h]]-1;
    }
}
void Converted_to_Decimal(int E[],int B[][m],int i,int k)//O(d)+O(d)=O(d)
{//此函式是將十進位制數以2維陣列B的形式存放。
    int x=E[i];
    for (int j=0;x>0;j++ )//O(di)迴圈了di次 di為當前數的位數
    {
        B[i][j]=x%10;
        x=x/10;
    }
    if (j<=k+1&&k+1!=m)
    {
        /*for (int k=j;k<m;k++)//O(d) 迴圈了d-j次 高位補0
        {
            B[i][k]=0;
        }*/
        B[i][k+1]=0;//如果A[i]位數小於將要COUNTING_SORT函式排序的那一位,那麼將該位重置為0
                    //相比以前需要迴圈置0,現在只需要一個常數時間內就可以達到原來的目的。
    }
}
void Radix_sort(int A[n],int B[][m],int C[n],int E[n],int d)
{
    d=m;int k=0;
    for ( int i=0;i<n;i++)//O(∑di)=O(n1) n1為總的位數。
    {
        Converted_to_Decimal(A,B,i,k);//O(di)  
    }
    for ( int j=0;j<d;j++)//O(d) d為位數
    {//因為d<=d位數的最小值<=k,(例如3<3位數最小值100)所以d<=n+k,內層迴圈O(n)+O(n+k)+O(n)+O(n)+O(d)+O(n)=O(n+k)
        cout<<endl;
        int k=Max(B,j);//O(n)
        COUNTING_SORT(B,C,k,j);//O(n+k)
        for ( i=0;i<n;i++)//O(n)
        {
            E[i]=A[C[i]];
        }
        /*if (j==d-1)//當迴圈到最高位時,直接跳出迴圈。不進行下面的迴圈,這樣縮短執行時間。
        {
            break;
        }*/
        for ( i=0;i<n;i++)//O(n)
        {
            Converted_to_Decimal(E,B,i,j);//O(n1)因為這n個數最低是1位數,所以n個1位數<=總位數和n1 O(n)=O(n1)
        }
        for (i=0;i<n;i++)//O(n)
        {
            A[i]=E[i];
        }
    }//所以Radix_sort時間複雜度為O(d(n+k))
    //O(n1d)+O(d(n1+k))=O(d(n1+k)) 當d為常數且k=O(n)時,時間複雜度為線性時間O(n1)
}
void main()
{
    srand( (unsigned)time( NULL ) );
    int A[n]={0};
    for (int i=0;i<n;i++)
    {
        A[i]=rand()%10000;
    }
    //int A[n]={1,45,23,8,120,78,9,4,25,200,9,10,34,58,78};
    int C[n]={0},E[n]={0},B[n][m]={0};
    Radix_sort(A,B,C,E,m);
    for ( i=0;i<n;i++)
    {
        cout<<A[i]<<" ";
    }  
}

每次呼叫Converted_to_Decimal函式都迴圈了di次,di是陣列第i個數的位數。在Radix_sort函式裡面,呼叫了n次Converted_to_Decimal函式,那麼這樣O(∑di)=O(n1) n1為總的位數。對於基數排序,需要對陣列最大位數d迴圈d次才可以將整個陣列好序,每迴圈一次就要呼叫n次Converted_to_Decimal函式其執行時間為O(n1) ,那麼迴圈d次,就是O(dn1),進行基數排序和計數排序還需要滿足兩個前提條件才可以使程式執行呈現線性時間,第1點是最大整數k=O(n),第2點d應該是常數,這樣 O(dn1)=O(n1)

b.給定一個字串陣列,其中不同的字串所包含的字元數可能不同,但所有字串中的總字元個數為n。設計一個演算法,使其可以在O(n)時間內該陣列進行排序。

 #include <iostream>  
using namespace std; 
const len=10,bit=5,n=26;
void Bubble_sort(char B[n][len][bit],int m);//首字母為B[n],比如B[0][len][bit]='A'
void BucketSort(char arr[len][bit],char B[n][len][bit]);
int my_strcmp(char *p1, char *p2);
void main()
{
    char arr[len][bit]={"aBc","cdE","Aa","CCFC","X","Bc","SDkF","BvX","CCEF","QW"};
    char B[n][len][bit]={0};
    BucketSort(arr,B);
    for (int i=0;i<len;i++)
    {
        for (int j=0;j<bit;j++)
        {
            if ((arr[i][j]>='A'&&arr[i][j]<='Z')||(arr[i][j]>='a'&&arr[i][j]<='z'))
            {
                cout<<arr[i][j];
            }
        }
        cout<<" ";
    }
}
void BucketSort(char arr[len][bit],char B[n][len][bit])
{
    int t=0,k=0,A[n]={0};
    for (int i=0;i<len;i++)//len代表字串元素個數n1 
    {//這個迴圈 O(len*bit)=O(n1*k) 
        if (arr[i][0]<='z'&&arr[i][0]>='a')
        {
            t=arr[i][0]-'a';
        }
        else if(arr[i][0]<='Z'&&arr[i][0]>='A')
        {
            t=arr[i][0]-'A';
        }
        k=A[t];
        for (int j=0;j<bit;j++)//bit代表字串長度k
        {
            B[t][A[t]][j]=arr[i][j];          
        }
        A[t]++;         
    }
    for (int j=0;j<n;j++)//O(n*len*bit)n代表26個字母,肯定是常數c,len代表元素個數n1,bit代表位數也就是字串長度k
    {//O(ckn1) c是常數,我們假設字串長度k也為常數,那麼O(ckn1)=O(n1)就是線性時間了。字串個數n1<=字串總的字元數n,所以O(n1)=O(n)
        Bubble_sort(B,j);//O(len*bit) 氣泡排序法的時間複雜度O(nj^2)
    }
    i=0;
    for (int a=0;a<n;a++)//這個迴圈分析同上
    {
        for (int b=0;b<len;b++)
        {
            if (B[a][b][0]!='\0'&&i!=len)
            {
                for (int c=0;c<bit;c++)
                {
                    arr[i][c]=B[a][b][c]; 
                }
                i++;
            }
        }
    }
}
void Bubble_sort(char B[n][len][bit],int m)//首字母為B[n],比如B[0][len][bit]='A'  
{   
    for (int j=0;B[m][j][0]!='\0';j++)//O(len)   
    {   
        for (int i=j+1;B[m][i][0]!='\0';i++)
        {
            if(my_strcmp(B[m][i],B[m][j])<0)
            {
                char temp[100];
                strcpy(temp,B[m][i]);
                strcpy(B[m][i],B[m][j]);
                strcpy(B[m][j],temp ); 
            }
        } 
       
    } 
}
int my_strcmp(char *p1, char *p2)
{
    int result = 0;
    while (*p1 && *p2)
    {
        if ((*p1<='z'&&*p1>='a')&&(*p2<='z'&&*p2>='a'))
        {//先判斷p1與p2是否都是小寫?
            *p2-=32;*p1-=32;//小寫轉化成大寫。
            if (*p1 == *p2)
            {
                *p2+=32;*p1+=32;//恢復小寫
                p1++;
                p2++;
                
                continue;//如果相等,2個字串比較下一個字母,並且下面的程式將不執行。
            }
            else
            {
                result = *p1 - *p2;
                *p2+=32;*p1+=32;//恢復小寫。
                break;//如果不等,計算2者之差,然後跳出迴圈。
            }//下面幾個判斷中的continue與break的含義類似
        }
        if((*p1<='z'&&*p1>='a'))
        {//如果p1與p2中有大寫,那麼再判斷p1是小寫?
            *p1-=32;
            if (*p1 == *p2)
            {
                *p1+=32;
                p1++;
                p2++;            
                continue;
            }
            else
            {
                result = *p1 - *p2;
                *p1+=32;
                break;
            }
        }
        if (*p2<='z'&&*p2>='a')
        {//如果p1與p2中有大寫,那麼再判斷p2是小寫?
            *p2-=32;
            if (*p1 == *p2)
            {
                *p2+=32;
                p1++;
                p2++;                
                continue;
            }
            else
            {
                result = *p1 - *p2;
                *p2+=32;
                break;
            }
        }
        if (*p1 == *p2)
        {
            p1++;
            p2++;
            continue;
        }
        else
        {
            result = *p1 - *p2;
            break;
        }
    }
    return result;
} 

//此程式將同一種字母的大寫和小寫視為相同處理,如果想區分大小寫,使同一個字母的大寫ASC碼總是小於小寫ASC碼,那麼直接用系統自帶strcmp函式即可。

對於字串長度為常數bit=k的字元陣列(字元陣列元素個數len=n)來說,BucketSort函式在劃分26個桶(26個字母)時,所用時間為O(n*k),我們設每個桶中字串個數k(i),所以對於每個桶我們可以呼叫時間為O(ni^2)=(n*k(i)),我們需要對26個字母在首位的字串陣列呼叫26次Bubble_sort函式,那麼有∑O(ni^2)+Θ(n*k)=∑O(ni^2)+Θ(n)=Θ(n) (此式具體證明和書中桶排序證明類似,這裡不做累述)

8-4 (水壺) 假設給了你n個紅色的水壺和n個藍色的水壺,它們的形狀和尺寸都各不相同。所有紅色水壺中盛有的水都不一樣多,藍色水壺也是如此。而且,對於每一個紅色水壺來說,都有一個對應的藍色水壺,兩者盛有一樣多的水,反之亦然。

 你的任務是找出所有的所盛水量一樣多的紅色水壺和藍色水壺,並將它們配成一對。為此,可以執行如下操作:跳出一對水壺,其中一個是紅色的,一個是藍色的,將紅色水壺倒滿水,再將水倒入藍色水壺中。通過這一操作,可以判斷出這個紅色水壺是否比藍色水壺盛的水更多,或者兩者是一樣多的。假設這樣的比較需要花費一個單位時間。你的目標是找出一個演算法,它能夠用最少的比較次數來確定所有水壺的配對。注意,你不能直接比較兩個紅色或者兩個藍色水壺。

a.設計一個確定性演算法,它能夠用Θ(n^2)次比較來完成所有水壺的配對。

在進行比較前,首先固定紅藍水壺的原有順序。然後取出第一個紅水壺,與n個藍水壺進行比較,根據題意,總能找到一個藍水壺和這個紅水壺
匹配,然後將已配對的紅水壺和藍水壺放到一邊,對剩下的n-1個紅藍水壺再次按照上面的方法進行比較。這樣一直進行到藍水壺剩0個代表都檢測
完畢了。

以下是a程式碼:

#include <iostream>
using namespace std;
const n=10;
void Kettle_a(int A[],int B[])
{
   for (int i=0;i<n;i++)//O(n)
   {
       for (int j=0;j<n;j++)//O(n)
       {
           if (B[j]==0)//判斷B[j]水壺是否已經被檢測過,如果檢測過,下面的if判斷將不執行。
           {
               continue;
           }
           if (A[i]==B[j])//判斷A與B水壺到底哪兩個是匹配的?
           {
               B[j]=0;//若匹配,那麼B[j]置空,說明已經被調查過了。
               cout<<"A["<<i<<"]=B["<<j<<"]="<<A[i]<<endl;//輸出剛被調查的兩個匹配水壺。
           }
       }
   }//O(n^2)
}
void main()
{
    int A[n]={20,1,5,8,19,4,7,23,18,10};//陣列中的整數代表水壺的容量 比如A[0]=20升。
    int B[n]={23,7,5,10,20,18,1,4,8,19};//同上
    Kettle_a(A,B);
}

b.證明:解決該問題演算法的比較次數下界為Ω(nlgn).

水壺的比較可以看做一顆決策樹,對於n個水壺來說,有n!種排列方式,每次紅藍水壺比較可以產生3種大小關係(>,<,=),那麼決策樹的下界在第八章開篇已經有所闡述了,下界就是Ω(nlgn).

c.設計一個隨機演算法,其期望的比較次數為Ο(nlgn),並證明這個界是正確的,對你的演算法來說,最壞情況下得比較次數是多少?

從期望時間來看,快速排序最合適,並且最壞時間是Ο(n^2).

我們分步:1.從紅色水壺中隨機選擇一個水壺作為主元。

                    2.從藍色水壺中找到與紅水壺主元容量一樣的水壺。

                    3.完成藍色水壺組的劃分操作。(比紅水壺主元容量大的在右,比A主元容量小的在左)

                    4.重複上面123來對紅水壺組進行相同的三步操作,來完成一次PARTITION函式劃分紅水壺組與藍水壺組操作。

                    5.對劃分後的紅藍水壺組做遞迴,不斷重複上述4步驟,然後同時完成對紅與藍水壺的排序操作,這樣按順序紅藍水壺一樣容量的一一對應完成配對。                 

以下是程式碼:

 #include <iostream>
#include <time.h>
using namespace std;
const n=10;
int RANDOM(int p,int r)
{
   int t=rand()%(r-p+1)+p;
   return t;
}
int PARTITION(int A[],int B[],int p,int r)//劃分操作的時間複雜度還是cn,只不過常數項大一些,以因為多了幾個迴圈
{
    int q=RANDOM(p,r);
    int x=A[q],t=0;//選擇A水壺作為B主元。
    for (int j=p;j<r;j++)
    {
        if (B[j]==x)//找到B水壺容量和A主元一樣的B中的位置,然後和B[r]交換位置。
        {
            swap(B[j],B[r]);
        }
    }
    int i=p-1;
    for ( j=p;j<=r-1;j++)//將B中以A主元為界,大於A主元的放到左邊,小於A主元的放在右邊。
    {
        if (B[j]<=x)//A水壺的某個水壺作為主元與B水壺的第j個進行比較。
        {
            i++;
            swap(B[i],B[j]);
        }
    }
    swap(B[i+1],B[r]);//這次交換過後,完成了B中以A主元為界的劃分。
    int y=B[i+1];//選取B水壺作為A主元。
    for ( j=p;j<r;j++)
    {
        if (A[j]==y)//和上面的B水壺排序類似
        {
            swap(A[j],A[r]);
        }
    }
    i=p-1;
    for ( j=p;j<=r-1;j++)//和上面的B水壺排序類似
    {
        if (A[j]<=y)
        {
            i++;
            swap(A[i],A[j]);
        }
    }
    swap(A[i+1],A[r]);
    return i+1;//最後,我們肯定的一點是A與B選擇的主元是一樣的,所以A與B有相同的劃分。
}
void QUICKSORT(int A[],int B[],int p,int r)//2T(n)+cn<=T(n)<=T(n-1)+T(0)+cn
{                                          //所以nlgn<=T(n)<=cn^2
    if (p<r)
    {
        int q=PARTITION(A,B,p,r);
        QUICKSORT(A,B,p,q-1);
        QUICKSORT(A,B,q+1,r);
    }
}


void main()
{
    //srand((unsigned)time(NULL)); 
    int A[n]={20,1,5,8,19,4,7,23,18,10};//陣列中的整數代表水壺的容量 比如A[0]=20升。
    int B[n]={23,7,5,10,20,18,1,4,8,19};//同上
    QUICKSORT(A,B,0,n-1);//經過對水壺容量的排序後,A的按順序對應的每一個位置和B按順序從小到大對應的水壺已經配對。
    for (int i=0;i<n;i++)
    {
        cout<<A[i]<<" ";
    }
    cout<<endl;
    for (i=0;i<n;i++)
    {
        cout<<B[i]<<" ";
    }
    cout<<endl;
}

以上程式完全符合快速排序性質,只不過劃分操作時的時間Θ(n)中常數項多一些,因為多了幾個迴圈。所以總的期望時間Ο(nlgn)常數項大些,快排的時間以前書中已經證明過了

8-5(平均排序)假設我們不是要完全排序一個數組,而只是要求陣列中的元素在平均情況下是升序的。更準確地說,如果對所有的i=1,2,...n-k有下式成立,我們就稱一個包含n個元素的陣列A為k排序的:

a.一個數組是1排序的,表示什麼含義?

1排序說明 A[i]≤A[i+1] 陣列在任何情況下都是非降序排列的。

b.給出對數字1,2....10的一個排列,它是2排序的,但不是完全有序的。

1,3,2,4,6,5,7,9,8,10

c.證明:一個包含n個元素的陣列是k排列的,當僅當對所有的ι=1,2...n-k,有A[i]≤A[i+k].

兩邊∑式展開後,左右兩邊個剩一項 也就是最終結論:A[i]≤A[i+k] 

d.設計一個演算法,它能在Ο(nlg(n/k))時間內對一個包含n個元素陣列進行k排序。當k是一個常數時,也可以給出k排序演算法的下界。

#include <iostream>
#include <time.h>
using namespace std;
const n=10;
int PARTITION(int A[],int p,int r)
{
    int x=A[r];
    int i=p-1;
    for (int j=p;j<=r-1;j++)//O(n)
    {
        if (A[j]<=x)
        {
            i++;
            swap(A[i],A[j]);
        }
    }
    swap(A[i+1],A[r]);
    return i+1;
}
void QUICKSORT(int A[],int p,int r,int k)
{
    if (p<r&&r-p>=k)//小於等於k時停止遞迴。其遞迴深度為lg(n/k)。
    {
        int q=PARTITION(A,p,r);
        QUICKSORT(A,p,q-1,k);
        QUICKSORT(A,q+1,r,k);
    }
}
void Print(int A[])
{
    for (int i=0;i<n;i++)
    {
        cout<<A[i]<<" ";
    }
}
void main()
{
    int A[n]={0};
    srand( (unsigned)time( NULL ) );
    for (int i=0;i<n;i++)
    {
        A[i]=rand()%100;
    }
    Print(A);
    cout<<endl;
    QUICKSORT(A,0,n-1,3);//k=3
    Print(A);
    cout<<endl;
}

其實不用快排用歸併排序也可以實現。

e.證明:我們可以在Ο(nlgk)時間內對一個長度為n的k排序陣列進行全排序。

將k排序陣列分為k組有序子陣列,每組Ai,A(i+k)....A(n-k+i) (i=1,2...k),進行最小堆的k路歸併後,利用6.5-9結論可知時間複雜度O(nlgk)

f.證明:當k是一個常數時,對包含n個元素的陣列進行k排序需要Ω(nlgn)的時間。

可以用決策樹模型。可以看成對於n個數分成k組數,分別對每組n/k個數進行排序,所用方法是決策樹,這n/k個數有(n/k)!種排列,其下界為Ω((n/k)lg(n/k)),那麼對於k組數非嚴格證明就是把這k組數下界相加,那麼就有Ω(k(n/k)lg(n/k))=Ω(nlg(n/k))因為k是常數,對於足夠大的n 常數k忽略不計,所以有Ω(nlg(n/k))=Ω(nlg(n))

8-6 (合併有序列表的下界)合併兩個有序列表是我們經常會遇到的問題。作為MERGE-SORT的一個子過程,我們在2.3.1節中已經遇到過這一問題。對這一問題,我們將證明在最壞情況下,合併兩個都包含n個元素的有序列表所需的比較次數的下界是2n-1.

首先,利用決策樹來說明比較次數一個下界2n-ο(n).

a.給定2n個數,請算出共有多少種可能的方式將它們劃分成兩個有序的列表,其中每個列表都包含n個數。

從2n個數裡選取n個數分成一組共有C(2n,n)種方法,然後剩下n個數為一組。

b.利用決策樹和(a)的答案,證明:任何能夠正確合併兩個有序列表的演算法都至少要進行2n-ο(n)次比較。

合併前,有C(2n,n)種方法排列這兩組有序列表,將此列表看成決策樹,所以有C(2n,n)個葉子結點,C(2n,n)<2^h <=> h>lg2n!-2lgn!


 現在我們來給出一個更緊缺界2n-1

c.請說明:如果兩個元素在有序序列中是連續的,且它們分別來自不同的列表,則它們必須進行比較。

網上查了貌似沒有答案,所以自己寫了個,寫得不好莫要見笑。兩個元素在有序序列是連續的。書中沒有給出有序序列中兩個元素是連續的概念,我的理解是這兩個元素和挨著的元素連續(設An,Bn)An與(B(n-1)或Bn或B(n+1)),Bn與(A(n-1)或An或A(n+1))都可以稱為連續的,而An與B(n+2)或Bn與A(n+2)是不連續的,並且(An與Bn的值非常接近)不存在d∈(An,Bn)且d∈(兩個有序序列)。如果An跳過了與它連續的兩個元素Bn與B(n+1),直接與B(n+2)比較,那麼An與未進行比較的兩個元素Bn與B(n-1)必然形成了一段無序序列。同理Bn跳過了與它連續的兩個元素An與A(n+1)也是一樣的。

d 利用你對上一部分的回答,說明合併兩個有序表時的比較次數下界為2n-1

完全沒有思路。。。

8-7  這道題是第三版新增加的。

(0-1排序引理和列排序)針對兩個陣列元素A[i]和A[j](i<j)的比較交換操作的形式如下:

COMPARE-EXCHANGE(A,i,j)

1.if A[i]和A[j]

2.   exchange A[i]withA[j]

經過比較交換操作之後,我們得到A[i]≤A[j]。

     遺忘比較交換演算法是指演算法只按照事先定義好的操作執行,即需要比較的位置下標必須事先確定好。雖然演算法可能依靠待排序元素個數,但它不能依賴排序元素的值,也不能依賴任何之前的比較交換操作的結果。例如,下面是一個基於遺忘比較交換演算法的插入排序:

INSERTION-SORT(A)

1 for j=2 to A.length

2.   for i=j-1downto 1

3.          COMPARE-EXCHANGE(A,i,i+1)

    0-1排序引理提供了有力的方法來證明一個遺忘比較交換演算法可以產生正確的排序結果。該引理表明,如果一個遺忘比較交換演算法能夠對所有隻包含0和1的輸入序列排序,那麼它也可以對包含任意值的輸入序列排序。

    你可以用反例來證明0-1排序引理:如果一個遺忘比較交換演算法不能對一個包含任意值得序列進行排序,那麼它也不能對某個0-1序列進行排序。不妨假設一個遺忘比較交換演算法X未能對陣列A[1..n]排序。設A[p]是演算法X未能將其放到正確位置的最小的元素,而A[q]是被演算法X放在A[q]是被演算法X放在A[p]原本應該在的位置上的元素。定義一個只包含0和1的陣列B[1..n]如下:B[i]=0 若A[j]≤A[p]  B[i]=1 若A[j]>A[p]。

    a.討論:當A[q]>A[p]時,有 B[p]=0 和B[q]=1.

分兩種情況討論:
當A[q]>A[p]時,設i=q時,有A[i]>A[p],那麼根據B的定義有B[i]=1 <=> B[q]=1
當A[q]>A[p]時,設i=p時,有A[q]>A[i](<=>A[i]<A[q]),這裡的q相當於原B定義的p,那麼根據B的定義有B[i]=0 <=> B[p]=0 

  b.為了完成0-1排序引理的證明,請先證明演算法X不能對陣列B正確排序。

根據我們對陣列B的定義,如果有A[i]≤A[j]則就有B[i]≤B[j],因此演算法X對於陣列A和陣列B進行交換的順序都相同。所以在陣列A上產生的輸出...A[q]...A[p]...那麼也有陣列B產生相同的輸出順序...B[q]...B[p]...,因為演算法X不能對包含任意值的陣列A進行排序,對於相同的排列順序B也就不能正確排序。

      現在,需要用0-1排序引理來證明一個特別的排序演算法的正確性。列排序演算法是用於包含n個元素的矩形陣列的排序。這一矩形陣列有r行s列(因此n=rs),滿足下列三個限制條件:r必須是偶數

       s必須是r的因子

       r≥2s^2

當列排序完成時,矩形陣列是列優先有序的,按照列從上到下,從左到右,都是單調遞增的。

     如果不包括n的值計算,列排序需要8步操作。所有奇數步都一樣;對每一列單獨進行排序。每一個偶數步是一個特定的排列。具體如下:

1.對每一列進行排序。

2.轉置這個矩形陣列,並重新規整化為r行s列的形式。也就是說,首先將最左邊的一列放在前r/s行,然後將下一列放在第二個r/s行,以此類推。

3.對每一列進行排序。

4.執行第2步排列操作的逆操作。

5.對每一列進行排序。

6.將每一列的上半部分移到同一列的下半部分位置,將每一列的下半部分移到下一列的上半部分,並將最左邊一列的上半部分置空。此時,最後一列的下半部分成為新的最右列的上半部分,新的最右列的下半部分為空。

7.對每一列進行排序。

8.執行第6步排列操作的逆操作。

       圖8-5顯示了一個在r=6和s=3時的列排序步驟(即使這個例子違背了 r≥2s^2的條件,列排序仍然有效。)


c.討論:即使不知道奇數步採用了什麼排序演算法,我們也可以把列排序看做一種遺忘比較交換演算法。

奇數步可能沒有采用遺忘比較演算法,但是根據遺忘比較演算法定義需要比較的位置下標在奇數步排序後已經被確定好了,而偶數步用的排序方法是固定的,所以不需要依賴任何之前的比較交換操作的結果,更不依賴於排序元素的值。所以總體來看這8步是可以看做一種遺忘比較交換演算法。

    雖然似乎很難讓人相信列排序也能實現排序,但是你可以利用0-1排序引理來證明這一點。因為列排序可以看做是一種遺忘比較交換演算法,所以我們可以使用0-1排序引理。

下面一些定義有助於你使用這一引理。如果陣列某個區域只包含全0或全1,我們定義這個區域是乾淨的。否則,如果這個區域包含的是0和1混合的,則稱這個區域是髒的。這裡,假設輸入資料只包含0和1,且輸入資料能夠被轉換為r行s列。

d.證明:經過第1-3步,陣列由三部分組成,頂部一些由全0組成的乾淨行,底部由一些全1組成的乾淨行,以及中間最多s行髒行。

第一步過後,絕大部分的0在矩陣中的最前幾個乾淨的行,絕大部分的1在矩陣的最後幾個乾淨的行,而中間有一些行可能是由0與1混合的髒行

第二步過後,各行之間遵循這樣一種規律,1.最上面的是一些乾淨的0行(純0),2.然後乾淨的0開始變髒混合一些1形成髒行(0逐漸少1逐漸多),3.然後髒行逐漸乾淨(直到最終為純1為止)。這樣在第一步中的第一列在第二步中轉化為數行輸出完畢。然後對第一步的第二列實現同樣的上述操作。。。。上面3點不斷重複迴圈直到最終整個矩陣按照步驟2排列好了。一共有s組由以上3點(1.純0,2.0->1過渡,3.純1)組成的行,而每組含有r/s行,每組中最多隻有一行為過渡(0->1)行(不存在2行以上的0->1過渡行,因為如果存在,比如有2行是過渡行,那麼需要從0到1然後又從0到1,這樣原來第一步的那一列就根本沒有排好序),所以對於這s組來說,就有s行是髒行。

第三步過後,和第一步有著類似的結果就是前面一些行是含有純0的乾淨的行,然後中間是由0與1混合而成的共s行髒行(0逐漸變少1逐漸增多),最後一些行含有純1的乾淨的行。

e.證明:經過第4步之後,如果按照列優先原則讀取陣列,先讀到的是全0的乾淨區域,最後是全1的乾淨區域,中間是由最多s^2個元素組成的髒的區域。

第四步由於是第二步的逆操作,將每r/s行按順序放入每一列,那麼其中有s行是髒行轉成列的形式,因為只是存放的形式變了(由行轉成列),其中並沒有對其排序,髒行的元素個數是不變的。那麼只要計算下第三步過後髒行元素個數即可,第三步過後,s行是髒行,同時這個矩陣總共有r行s列,那麼這個矩陣其中的s髒行也應該有s列,元素個數=s髒行Xs列=s^2。所以第四步過後有最多s^2個元素組成的區域。

f.證明:第5-8步產生一個全排序的0-1輸出,並得到結論,列排序可以正確的對任意輸入值排序。

第五步排序包含髒區域的列並且在排序過程中髒區域沒有增加。

第六步將整個髒區域移動到同一列中。

第七步對此矩陣再次進行排序,髒區域也變得有序,

第八步再移動回來。

並且在5-8步排序過程中髒區域最多s^2≤r/2,也就是5-8步髒區域元素個數沒有超過r/2。既然由0-1組成的矩陣已經排好序了。那麼根據0-1排序引理知,如果一個遺忘比較演算法能夠對所有隻包含0和1的輸入序列排序,那麼它也可以對包含任意值得輸入序列排序。

g.現在假設s不能被r整除。證明:經過第1-3步,陣列的頂部有一些全0的乾淨行,底部有一些全1的乾淨行,中間是最多2s-1行髒行。那麼與s相比,在s不能被r整除時,r至少要多大才能保證列排序的正確性?

如果中間最多有2s-1髒行,那麼對於r行s列的矩陣來說,中間髒區域的元素個數應該是(2s-1)髒行xs列=2s^2-s個,根據f)的結論知道髒區域元素個數不超過r/2,那麼有2s^2-s≤r/2,這樣r≥4s^2-2s.但是這個結論的大前提是“中間最多有2s-1髒行”,可我不太明白為什麼是2s-1髒行?由請高手留言。謝謝!

h.對第1步做一個簡單修改,使得我們可以在s不能被r整除時,也保證r ≥2s^2,並且證明在這一修改後,列排序仍然正確

完全不懂。懂的請留言謝謝!

相關推薦

演算法導論思考題

8-1(比較排序的概率下界) 在這一問題中,我們將證明對於給定的n個互異的輸入元素,任何確定或隨機的比較排序演算法,其概率執行時間都有下界Ω(nlgn)。首先分析一個確定的比較排序演算法A,其決策樹為Ta,假設A的輸入的每一種排列情況都是等可能的。 a) 假設Ta的每個葉子

演算法導論 23 最小生成樹 思考題

23-1 次最優的最小生成樹     (c)根據普利姆演算法計算出最小生成樹,並得到樹的parent陣列(裡面記錄了各頂點的父頂點)。 運用動態規劃方法即可,狀態轉移方程如下: 設頂點個數為V,那麼時間複雜度為O(V2) 。 (d) 1、根據普利姆演算法計算得出最

演算法導論 十三:紅黑樹 筆記(紅黑樹的性質、旋轉、插入、刪除)

紅黑樹(red-black tree) 是許多“平衡的”查詢樹中的一種,它能保證在最壞情況下,基本的動態集合操作的時間為O(lgn) 。 紅黑樹的性質: 紅黑樹是一種二叉查詢樹,但在每個結點上增加一個儲存位表示結點的顏色,可以是RED或BLACK 。通過對任何一條從根到葉子的路徑上各個結

演算法導論 :快速排序 筆記(快速排序的描述、快速排序的效能、快速排序的隨機化版本、快速排序分析)

快速排序的最壞情況時間複雜度為Θ(n^2)。雖然最壞情況時間複雜度很差,但是快速排序通常是實際排序應用中最好的選擇,因為它的平均效能很好。它的期望執行時間複雜度為Θ(n lg n),而且Θ(n lg n)中蘊含的常數因子非常小,而且它還是原址排序的。 快速排序是一種排序演算法,對包含n個數的

演算法導論 :堆排序 筆記(堆、維護堆的性質、建堆、堆排序演算法、優先順序佇列、堆排序的程式碼實現)

堆排序(heapsort) 像合併排序而不像插入順序,堆排序的執行時間為O(nlgn) 。像插入排序而不像合併排序,它是一種原地( in place) 排序演算法:在任何時候,陣列中只有常數個元素儲存在輸入陣列以外。 堆: (二叉)堆資料結構是一種陣列物件,它可以被視為一棵完全二叉樹。樹

演算法導論 :概率分析和隨機演算法 筆記(僱傭問題、指示器隨機變數、隨機演算法、概率分析和指示器隨機變數的進一步使用)

僱傭問題: 假設你需要僱用一名新的辦公室助理。你先前的僱傭嘗試都以失敗告終,所以你決定找一個僱用代理。僱用代理每天給你推薦一個應聘者。你會面試這個人,然後決定要不要僱用他。你必須付給僱用代理一小筆費用來面試應聘者。要真正地僱用一個應聘者則要花更多的錢,因為你必須辭掉目前的辦公室助理,還要付一

演算法導論 :遞迴式 筆記(代換法、遞迴樹方法、主方法、主定理的證明)

三種解遞迴式的方法:代換法、遞迴樹方法、主方法。 代換法: 用代換法解遞迴式需要兩個步驟: 猜測解的形式; 用數學歸納法找出使解真正有效的常數。 如: T(n) = 2T(n/2) + n,這個是合併排序的執行時間的遞迴表示式。歸併排序法的執行時間是O(nlgn),那麼我

演算法導論 :函式的增長 筆記(Θ記號、O記號、Ω記號、o記號、ω記號、漸近記號的性質、標準記號與常用函式)

Θ記號: 該記號圓圈中是個M。Θ記號漸近地給出一個函式的上界和下界。 對於一個給定的函式g(n),我們用Θ(g(n))來表示以下函式的集合: Θ(g(n))={f(n):存在正常量c1、c2和n0,使得對於所有n⩾n0,有0⩽c1g(n)⩽f(n)⩽c2g(n)}。 即若存在正常

資料結構和演算法 圖論演算法

9.1 若干定義 圖的定義:一個圖(Graph) G=(V,E)是由頂點的集合V和邊Edge的集合E組成的。每一條邊就是一個頂點對(v,w),其中(v,w) ∈E。有時候也把邊叫做弧。如果頂點對是有序的,那麼圖就是有向的。有的圖也叫做有向圖。頂點w和頂點v鄰接當且僅當(v,w)

2018/11/29 演算法時空(2) 演算法導論 函式的增長

漸進記號:   O記號:   歐米茄記號: 注意: O記號是複雜度函式的上限, 歐米茄記號是複雜度函式的下限. 等式/不等式漸進記號:  極限的定義: 通過極限的方法, 來求複雜度函式.   當極限的值是一個大於零

【學習筆記】演算法導論2演算法基礎

//====================================== //Ch2_1_Basic_Sort_Algorthm //====================================== #include<iostream> #

為什麼我要放棄javaScript資料結構與演算法)—— 樹

之前介紹了一些順序資料結構,介紹的第一個非順序資料結構是散列表。本章才會學習另一種非順序資料結構——樹,它對於儲存需要快速尋找的資料非常有用。 本章內容 樹的相關術語 建立樹資料結構 樹的遍歷 新增和移除書的節點 AVL 樹 第八章 樹 樹資料結構 樹是一種分層資料的抽象模型。現實生活中最常見的樹的典型例

演算法導論 13 紅黑樹(圖文詳細解說)

1、二叉查詢樹的不足 二叉查詢樹的基本操作包括搜尋、插入、刪除、取最大和最小值等都能夠在O(h)(h為樹的高度)時間複雜度內實現,因此能在期望時間O(lgn)下實現,但是二叉查詢樹的平衡性在這些操作中並沒有得到維護,其高度可能會變得很高,當其高度較高時,二叉查詢樹的效能就未

演算法導論 10 10.2 連結串列

一、概念 (1)陣列的線性序是由陣列的下標決定的,連結串列中的順序是由各物件中的指標所決定的 (2)連結串列結點結構 node *prev; node *next; int key; (3)連結串列

演算法導論 18 B樹

一、定義 1、B樹 B樹是為磁碟或其它直接存取輔助儲存裝置而設計的一種平衡查詢樹,主要特點是降低磁碟I/O操作次數。 B樹以自然的方式推廣二叉查詢樹。 B樹的分支因子由磁碟特性所決定。  2、B數的資料結構 int n:當前儲存在結點x中的關鍵字數 key[N]:n個關鍵,

演算法導論12:二叉搜尋樹

基本性質 左子樹 < 根 < 右子樹 基本操作 O(logn) 1.查詢最大、最小關鍵字元素 根據二叉樹的基本性質,向左子樹或右子樹遞迴即可 2.查詢前驅和後繼 查詢結點X的後繼Y分為兩種情況: ①右結點存在,即只需要找到右子樹中最小的元素就好

計算機導論-總結

詳情 次數 使用 nbsp 優化問題 突變 bsp 怎樣 們的 1. 學習了可求解問題、難求解問題和不可計算問題的概念:可求解問題 – 計算機在有限時間內能夠求解的問題;難求解問題 – 計算機在有限時間內不能求解的問題;不可計算問題 – 計算機完全不能求解的問題。 2

演算法導論15習題答案

15.2-2 Q:請給出一個遞迴演算法MATRIX-CHAIN-MULTIPLY(A,s,i,j),使之在給出矩陣序列<A1,A2,...,An>,和由MATRIX-CHAIN-ORDER計算出的表s, 以及下標i和j後,能得出一個最有的矩陣鏈乘法。(初始呼叫為MATRIX-CHAIN-MULT

演算法導論 20 斐波那契堆

一、綜述1.斐波那契堆斐波那契堆是可合併堆在不涉及刪除的操作(除去EXTRACT和DELETE)中,操作僅需O(1)的平攤執行時間當EXTRACT和DELETE的運算元目較小時斐波那契堆能得到較好的執行效率。斐波那契堆不能有效地支援SEARCH操作用於解決諸如最小生成樹和尋找

演算法導論-15-動態規劃-15.1 鋼條切割問題

一、綜述 動態規劃是通過組合子問題的解而解決整個問題的。 動態規劃對每個子問題只求解一次,將其結果儲存在一張表中。動態規劃通常用於最優化問題。動態規劃的設計步驟:a. 描述最優解的結構b. 遞迴定義最優解的值c. 按自底向上的方式計算最優解的值d. 由計算出的資訊構