圖解十大排序算法(希爾排序、快速排序、堆排序、歸併排序、基數排序等)

圖解十大排序算法

學了數據結構之後,決定好好再回顧一下這幾種排序算法,加深一下自己的印象的同時也希望能夠幫助到大家理解這些算法和思想。
如果發現錯誤可以私信或評論哈!

首先先介紹一下排序算法的分類:
在這裏插入圖片描述

我們接下來介紹的是內部排序中的算法。好的,那麼開始了!

插入排序

直接插入排序

直接插入排序是最簡單的排序方法,每次將一個待排序的記錄,插入已經排好的數據序列中,得到一個新的長度增1的有序表,如下所示:
在這裏插入圖片描述

算法步驟:

  • 設待排序的記錄存儲在數組r[1,…n]中,可以把第一個記錄r[1]作爲一個有序序列
  • 依次將r[i]插入已經拍好的有序序列中r[i,…(i-1)]中,並保持有序

圖解過程如下:
在這裏插入圖片描述
後續操作如上,最終得出升序排序結果:1、7 、 7* 、 12、 15、 19、 30 、45

程序如下:

注意elemtype爲自定義數據類型,如typedef int elemtype;

void InsertSort(elemtype arr[],int length)
{
    int j;
    for(int i=2;i<=length;i++)
    {
        arr[0] = arr[i];//設置監視哨
        j = i-1;   //從當前元素的前一個判斷
        while(arr[0] < arr[j] && j>0)  
        {//如果當前元素大於待插入元素,則後移一位
            arr[j+1] = arr[j];  
            j=j-1;
        }//否則插入到比待插入元素小的後面
        arr[j+1] = arr[0];
    }
}

折半插入排序

折半插入排序其實就是針對於直接插入排序中的查找部分進行了折半查找優化

圖解過程如下:

圖示表示已經到達對最後一個數據進行排序時的狀態!!
在這裏插入圖片描述

//折半插入排序
void BinSort(elemtype arr[],int len)
{
    int j,x,low,high,mid;
    for(int i=2;i<=len;i++)
    {
        arr[0] = arr[i];  //哨兵記錄
        low = 1;
        high = i-1;
        while(low <= high)  //確定插入的位置
        {
            mid = (low + high)/2;
            if(arr[0] < arr[mid])
                high = mid - 1;
            else
                low = mid + 1;
        }
        for(j = i-1;j>=low;j--)  //將該位置之後的元素後移
            arr[j+1]=arr[j];
        arr[low] = arr[0];  //插入
    }
}

鏈表插入排序

鏈表插入排序原理同直接插入排序相同,區別就是鏈表的插入操作,不需要像數組一樣將其他元素後移,插入過程時間複雜度小。

圖解過程:
在這裏插入圖片描述

代碼如下:

insert指向待插入結點;

n指向下一個待插入結點;

typedef struct Node{
    elemtype data;
    struct Node * next;
}Node,*Linklist;
//鏈表插入排序
void CreatLinklist(Linklist *L)//創建一個鏈表
{
    elemtype e;
    Node * add;
    (*L) = (Linklist)malloc(sizeof(Node));
    (*L)->next = NULL;
    while(scanf("%d",&e) != EOF)
    {
        add = (Linklist)malloc(sizeof(Node));
        add->data = e;
        add->next = (*L)->next;
        (*L)->next = add;
    }
}
bool LinkInsertSort(Linklist *L)//鏈表插入排序
{
    Node *insert,*temp,*n;
    if((*L)->next == NULL || (*L)->next->next == NULL)  //鏈表沒有結點或者只有一個則不需要排序
        return false;
    temp = (*L);
    insert = temp->next->next;
    n = insert->next;
    temp->next->next = NULL;
    while(insert)
    {
        while(temp->next != NULL)
        {
            if(insert->data >= temp->next->data)
                temp = temp->next;
            else
            {
                insert->next = temp->next;
                temp->next = insert;
                break;
            }
        }
        if(temp->next == NULL)
        {
            insert->next = NULL;
            temp->next = insert;
        }
        insert = n;
        if(n != NULL)
            n = n->next;
        temp = (*L);
    }
    return true;
}

void PrintLink(Linklist L)//打印鏈表
{
    Node * temp;
    temp = L->next;
    while(temp)
    {
        printf("%d ",temp->data);
        temp = temp->next;
    }
    printf("\n");
}

希爾排序

直接插入排序中,如果待排序的數據少並且基本有序,則排序效率較高。因此我們便需要想辦法優化,將其減少數據個數並且使其基本有序,這便有了希爾排序

希爾排序是將待排序記錄按下標的一定增量進行分組(減少其數據個數),並且對每組數據進行直接插入排序算法進行排序(使其基本有序)。隨着增量的減少,每組包含的數據越來越多,最終當增量爲1時,也就是對整個數組再進行一次直接插入排序,則完成排序任務。

算法原理

  1. 設待排序的記錄存儲存儲在數組r[1…n],中增量序列爲{d1,d2,d3,…,dt}。
  2. 第一趟取增量d1。所有間隔爲d1的記錄分在一組。對每組記錄進行直接插入排序。
  3. 第二趟取增量d2。所有間隔爲d2的記錄分在一組。對每組記錄進行直接插入排序。
  4. 依次進行下去,直到索取增量dt等於1。所有記錄在一組中進行直接插入排序。完成

假設初始序列爲21,23,42,52,13,53,67,45,24,9,取增量d{5,3,1}
在這裏插入圖片描述

代碼:

//希爾排序
void ShellSort(elemtype arr[],int n,int dt[],int t)
{
    for(int k=0;k<t;k++)
    {
        DtSort(arr,n,dt[k]);
    }

}
void DtSort(elemtype arr[],int n,int dt)
{
    int i,j;
    for(i=dt+1;i<=n;i++)
    {
        if(arr[i-dt] > arr[i])
        {
            arr[0] = arr[i];
            for(j=i-dt;j>0 && arr[j]>arr[0];j-=dt)
                arr[j+dt] = arr[j];
            arr[j+dt] = arr[0];
        }
    }
}

交換排序

冒泡排序

冒泡排序是一種最簡單的排序算法,通過兩兩比較,如果逆序就交換,使數據最大的泡泡先冒出來,因此得名冒泡排序!
在這裏插入圖片描述

代碼:

//冒泡排序
void BubleSort(elemtype arr[],int len)
{
    int i,j,temp;
    bool flag;
    i = len;
    flag = true;
    while(i>0 && flag)  //如果flag=false,則表示上一次排序沒有進行交換,即已經排好了
    {
        flag = false;  
        for(j=1;j<i;j++)
        {
            if(arr[j+1] < arr[j])
            {
                flag = true;   //如果
                temp = arr[j+1];
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
        i--;
    }
}

快速排序

快速排序的基本思想使一種分治的策略,即將一組序列分割成獨立的兩部分,其中一部分的數據比另一部分的數據小,然後再按照此方法對兩部分序列再進行分治,聰明的人都看出來了,這不就是遞歸調用嘛。

算法思想:
  1. 劃分:再r[low:high]中選一個元素作爲基準元素r[base],將數組劃分爲兩個數組r[low:base-1]和r[base+1:high],並且r[low:base-1]中的元素均小於r[base],r[base+1:high]中的元素均大於等於r[base]。
  2. 分治:對r[low:base-1]和r[base+1:high]分別再進行遞歸調用進行快速排序

基準元素的選取有很多鍾方式,可以選r[1],r[len],r[mid]等等,我們取第一個元素人[1]作爲基準元素base。

算法步驟:
  1. 首先取數組的第一個元素r[1]作爲基準元素。
  2. 從右向左掃描,找小於等於基準的數,如果找到r[i]和r[j]交換,i++。
  3. 從左向右掃描,找大於基準的數,如果找到則r[i]和r[j]交換,j++。
  4. 重複第二步和第三步操作,直到i和j重合,返回該該位置,該位置的數正好是基準元素。
  5. 至此,完成一趟排序。此時以mid爲界,將源序列分爲兩個子序列左側的元素小於等於基準元素。其中右側子序列元素大於基準元素,再分別對這兩個子序列進行快速排序。

設初始序列爲:21,23,42,52,13,53,67,45,24,9
在這裏插入圖片描述

代碼:

//快速排序
void QuickSort(elemtype arr[],int low,int high)
{
    int mid;
    if(low<high)
    {
        mid = Partition(arr,low,high);
        QuickSort(arr,low,mid-1);
        QuickSort(arr,mid+1,high);
    }
}
int Partition(elemtype arr[],int low,int high)
{
    int left=low,right=high,base = arr[low];
    int temp;
    while(left<right)
    {
        while(left<right && arr[right]>=base)
        {
            right--;
        }
        if(left<right)
        {
            temp = arr[right];
            arr[right] = arr[left];
            arr[left] = temp;
            left ++;
        }
        while(left<right && arr[left]<=base)
        {
            left++;
        }
        if(left<right)
        {
            temp = arr[right];
            arr[right] = arr[left];
            arr[left] = temp;
            right --;
        }
    }
}
算法優化:

由上述算法過程可知,在分治的過程中,其實就是不斷的將序列分爲大、小兩部分序列,操作就是不斷和base比較交換,因此我們可以優化算法減少交換次數。

優化前:right<base的話交換r[left]和r[right],left>base的話交換r[left]和r[right]

優化後:當遇到一組right<base並且left>base時交換r[left]和r[right]

過程詳見如下:

  1. 去基準數base
  2. 右走,找到小於基準數的數r[right]
  3. 左走,找到大於基準數的數r[left]
  4. 交換r[right]和r[left],left++,fight–
  5. 重複2~4操作直到left==right,如果r[left]>base,交換r[left-1]和r[low],返回mid=left-1,否則交換r[left]和r[low],返回mid=left。

過程圖解
在這裏插入圖片描述

優化後的int PartitionPlus(elemtype arr[],int low,int high)

int PartitionPlus(elemtype arr[],int low,int high)
{
    int left=low,right=high,base = arr[low];
    int temp;
    while(left<right)
    {
        while(left<right && arr[right]>=base)
        {
            right--;
        }
        while(left<right && arr[left]<=base)
        {
            left++;
        }
        if(left<right)
        {
            temp = arr[right];
            arr[right] = arr[left];
            arr[left] = temp;
            right --;
            left ++;
        }
    }
    if(arr[left]>base)
    {
        temp = arr[low];
        arr[low] = arr[left-1];
        arr[left-1] = temp;
        return left-1;
    }else
    {
        temp = arr[low];
        arr[low] = arr[left];
        arr[left] = temp;
        return left;
    }
}

選擇排序

簡單選擇排序

簡單選擇排序也是最簡單的一種排序方法。即每次選擇出最小的一個數放在數組最前面。算法原理大家都懂,就不放圖了(其實是懶得做了_)。

直接上代碼:

void SimpleSelectSort(elemtype arr[],int len)
{
    int i,j,min_tag,temp;
    for(i=1;i<len;i++)
    {
        min_tag = i;
        for(j=i+1;j<=len;j++)
        {
            if(arr[j]<arr[min_tag])
            {
                min_tag = j;
            }
        }
        if(min_tag != i)
        {
            temp = arr[i];
            arr[i] = arr[min_tag];
            arr[min_tag] = temp;
        }
    }
}

堆排序

堆排序是一種樹形選擇排序算法。

堆可以看作一課完全二叉樹的順序存儲結構。在這顆完全二叉樹中,如果每一個節點的值都大於等於左右孩子的值,稱爲最大堆(大根堆)。如果每一個節點的值都小於等於左右孩子的值,則稱爲最小堆(小根堆)。

其中堆排序用了完全二叉樹的一個非常重要的性質:

如果一個節點的下標爲i,其左孩子下標爲2i,有孩子爲2i+1,雙親下表爲i/2。

算法步驟

  1. 根據初始序列構建初始堆
  2. 堆頂和最後一個元素交換,即r[1]和r[n]交換,並存儲r[n],並將r[1…(n-1)]重新調整爲堆
  3. 堆頂和最後一個元素交換,即r[1]和r[n-1]交換,並存儲r[n-1],並將r[1…(n-2)]重新調整爲堆
  4. 循環n-1次,即直到堆中只有一個元素

從上述算法步驟瞭解到我們需要完成的操作有:

  • 創建堆的過程
  • 堆頂記錄存儲
  • 調整堆

交換操作之後會導致樹不在滿足堆的定義:
在這裏插入圖片描述

調整堆

調整堆又叫做下沉操作。

將堆頂與左右孩子比較,如果比孩子大,則已經調整爲堆;如果比孩子小,則與較大的孩子交換;然後繼續和孩子比較大小,直到比較到葉子節點。
在這裏插入圖片描述

構建初始堆

構建過程:

首先按照完全二叉樹的順序構建一棵完全二叉樹,然後從最後一個分支節點n/2開始調整堆依次將序號n/2-1,n/2-2,…,1的節點執行下沉操作調整爲堆

爲什麼要從最後一個分枝結點開始呢?因爲調整堆的前提是除了堆頂之外,其他結點都滿足最大堆的定義,只需要堆頂下沉即可。而葉子結點沒有孩子,因此可以視爲一個堆,從最後一個分支結點開始調整堆,調整後該結點以下的分支均滿足堆定義,其雙親節點調整時,其左右子樹均已經滿足最大堆的定義。

設初始序列爲{18,6,10,16,28,2,17,20,30,12}
在這裏插入圖片描述

排序

接下來就是最後一個操作了…做圖做的眼睛疼T T,加油。

過程就是堆頂與最後節點交換,同時存儲,然後堆頂下沉,直到只剩一個節點。
在這裏插入圖片描述
代碼:

//堆排序
void HeapSort(elemtype arr[],int n)
{
    int temp;
    CreatHeap(arr,n);//創建初始堆
    while(n>1)//循環n-1次
    {
        //交換
        temp = arr[1];
        arr[1] = arr[n];
        arr[n] = temp;
        n--;
        //下沉
        Sink(arr,1,n);
    }
}

void CreatHeap(elemtype arr[],int n)
{
    for(int i=n/2;i>0;i--)//從最後一個分支節點開始創建堆
    {
        Sink(arr,i,n);
    }
}
void Sink(elemtype arr[],int k,int n)
{
    int temp;
    while(2*k <= n)//左孩子爲2*k
    {
        int j=2*k;//j指向左孩子
        if(j+1<=n && arr[j]<arr[j+1])//如果有有孩子並且右孩子比左孩子大
        {
            j++;
        }
        if(arr[k]>arr[j])//雙親大於兩個孩子節點,已經爲堆
        {
            break;
        }else//否則交換
        {
            temp = arr[k];
            arr[k] = arr[j];
            arr[j] = temp;
        }
        k = j;//繼續下沉
    }
}

合併排序

合併排序也叫歸併排序,其採用分治的策略,將大問題分成若干個小問題,先解決小問題,再利用已經解決的小問題取解決大問題。總體思想就是先分後合,所謂天下之勢,乃是合久必分,分久必合^ _ ^。

算法思路

  1. 將待排序列分成規模大致相同的兩個子序列
  2. 遞歸分別對兩個子序列進行合併排序
  3. 將排好的序列逐個合併
圖解過程

在這裏插入圖片描述

下面我們來講以下合併過程,我們舉最後一次合併操作過程進行圖解。

在這裏插入圖片描述

代碼:

//歸併排序
void MergeSort(elemtype arr[],int low,int high)
{
    if(low <high)
    {
        int mid = (low+high)/2;
        MergeSort(arr,low,mid);
        MergeSort(arr,mid+1,high);
        Merge(arr,low,mid,high);
    }
}
void Merge(elemtype arr[],int low,int mid,int high)
{
    elemtype *save;
    save = (elemtype*)malloc(sizeof(elemtype)*(high-low+1));
    int left=low,right=mid+1,k=0;//
    while(left<=mid && right<=high)
    {
        if(arr[left] <= arr[right])
        {
            save[k++] = arr[left++];
        }else
        {
            save[k++] = arr[right++];
        }
    }
    while(left<=mid)
        save[k++] = arr[left++];
    while(right<=high)
        save[k++] = arr[right++];
    for(int i=low,k=0;i<=high;i++,k++)
    {
        arr[i]=save[k];
    }
    free(save);
}

分配排序

分配排序不需要比較數據,只是根據數據的大小,進行若干躺分配和收集實現排序。

桶排序

桶排序將待排序列劃分成若干個區間,每個區間可形象地看作一個桶如果桶中的記錄多於一個則使用較快的排序方法進行排序,然後再把每個桶中的記錄收集起來,最終得到有序序列。

假設有10個小學生的身高{151,145,149,167,153,178,164,149*,180,172},進行桶排序,先劃分區間[145-150]、(150-155]、(155-160]、(160-165]、(165-170]、(170-175]、(175-180]、(180-185]、(185-190]、(190-195]

分爲10個桶,放入桶中:

在這裏插入圖片描述

再分別對每個桶中的數據進行排序(很巧,數據取的不好,好像都是拍好了),然後從左往右收集:

得到{145,149,149*,151,153,164,167,172,178,180}

具體代碼就不講了,因爲我們着重講桶排序的延伸,基數排序

基數排序

基數排序可以看過桶排序的擴展,它是一種多關鍵字排序算法,如果按照多個關鍵字排序,則一次按照這些關鍵字進行排序。例如撲克牌排序,撲克牌由數字面值和花色兩跟關鍵字組成,可以先按照面值排序,再按照花色排序。

例如初始序列爲10個學生成績{68,75,54,70,83,48,80,12,75*,92}

算法步驟
  1. 求出待排序序列中最大關鍵字的位數d,然後從低位到高位進行基數排序。
  2. 按個位將關鍵字依次分配到桶中,然後將每個桶中的數據依次收集起來。
  3. 按十位將關鍵字依次分配到桶中,然後將每個桶中的數據依次收集起來。
  4. 依次下去,直到d位處理完畢,得到一個有序序列。
圖解算法

在這裏插入圖片描述

代碼:

//基數排序
int Maxbit(elemtype arr[],int n)
{
    int max=arr[1],digits=0;
    for(int i=1;i<=n;i++)
    {
        if(arr[i]>max)
            max = arr[i];
    }
    while(max != 0)
    {
        digits ++;
        max /= 10;
    }
    return digits;
}

int Bitnumber(elemtype x,int bit)
{
    int temp=1;
    for(int i=1;i<bit;i++)
        temp*=10;
    return (x/temp)%10;
}

void RadixSort(elemtype arr[],int n)
{
    int i,j,k,bit,maxbit;
    maxbit = Maxbit(arr,n);
    elemtype **save = (elemtype**)malloc(sizeof(elemtype*)* 10);
    for(i=0;i<10;i++)
        save[i] = (elemtype*)malloc(sizeof(elemtype)*(n+1));
    for(i=0;i<10;i++)
    {
        save[i][0]=0;//索引0是記錄當前桶中的數據個數
    }
    for(bit=1;bit<=maxbit;bit++)
    {
        for(j=1;j<=n;j++)//分配
        {
            int num = Bitnumber(arr[j],bit);
            int index= ++save[num][0];
            save[num][index] = arr[j];
        }
        for(i=0,j=1;i<10;i++)//收集
        {
            for(k=1;k <= save[i][0];k++)
                arr[j++]=save[i][k];
            save[i][0] = 0;//收集完了清空個數
        }
    }
	for(int i = 0; i < 10; ++i)
		free(save[i]);
	free(save);
}

時間複雜度、穩定性分析:

在這裏插入圖片描述
Over!
(覺得有用的夥伴也可以收藏贊下鴨^_^

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章