【資料結構基礎應用】【查詢和排序演算法】

拾牙慧者發表於2020-09-23

程式碼參考《妙趣橫生的演算法.C語言實現》


前言

本章總結查詢和排序演算法:順序查詢、折半查詢、直接插入排序、氣泡排序、簡單選擇排序、希爾排序、快速排序、堆排序以及排序演算法效能比較。


1、順序查詢

順序查詢就是在檔案的關鍵字結合key[1,2,…n]中找出與給定的關鍵字key相等的檔案記錄。

步驟描述:

1、從檔案的第一個記錄開始,將每個記錄的關鍵字與給定的關鍵字key進行比較

2、如果查詢到某個記錄的關鍵字等於key,則查詢成功,返回該記錄的地址。如果所有記錄的關鍵字都與key進行了比較,但都未匹配,則本次查詢失敗,返回失敗標記-1

//順序查詢:n表示記錄個數、key表示要查詢記錄的關鍵字、key[]為存放所有記錄關鍵字順序表。
int sq_search(keytyped key[],int n,keytype key)
{
	int i;
	for (i=0;i<n;i++)
	{
		if (key[i] == key)
		{
			return i;
		}
	}
	return -1;
}

用結構體描述:

typedef struct {
	keytype key;		//keytype型別的關鍵字key
	datatype data;		//記錄其中資訊
}RecordType;
int sq_search(RecordType r[], int n, keytype key)
{
	int i;
	for (i = 0;i < n;i++)
	{
		if (r[i].key == key)		//查詢成功
		{
			return i;
		}
	}
	return -1;
}
//缺點:平均查詢長度過大,查詢效率較低

2、折半查詢

只有在關鍵字的排序是有序的(遞增或遞減)情況下,才能應用折半查詢的演算法描述。

基本思想:

減少查詢序列的長度,分而治之進行關鍵字的查詢。

查詢過程:先去定待查詢記錄的所在反胃,然後逐漸縮小查詢的範圍,直到找到為止(也可能查詢失敗)

//基於遞增序列的折半查詢
//n表示記錄個數、k表示要查詢到的關鍵字、key[]關鍵字順序表
int bin_search(keytype key[], int n, keytype k)
{
	int low = 0, high = n - 1, mid;
	while (low <= high)
	{
		mid = (low+high) / 2;
		if (key[mid] == k)
			return mid;
		if (k > key[mid])
			low = mid + 1;		//在後半序列中查詢
		else
			high = mid - 1;
	}
	return -1;					//查詢失敗,返回-1
}

用結構體描述:

typedef struct {
	keytype key;			//關鍵字
	datatype data;			//記錄的資訊
}RecordType;

int bin_search(RecordType r[], int n, keytype key)
{
	int low = 0, high = n - 1, mid;
	while (low <= high)
	{
		mid = (low + high) / 2;
		if (r[mid].key == key)
			return mid;
		if (key > r[mid].key)
			low = mid + 1;		//在後半序列中查詢
		else
			high = mid - 1;
	}
	return -1;					//查詢失敗,返回-1
}

3、直接插入排序

排序可以理解為:
根據檔案記錄的關鍵字值得遞增或者遞減關係將檔案記錄的次序進行重新排列的過程。
或者是:將一個按值無序的資料序列轉換成為一個按值有序的資料序列的過程

直接插入排序(Straight Insertion Sort)是一種最簡單的排序方法,其基本操作是將一條記錄插入到已排好的有序表中,從而得到一個新的、記錄數量增1的有序表。

在日常生活中,經常碰到這樣一類排序問題:把新的資料插入到已經排好的資料列中。例如:一組從小到大排好順序的資料列{1,2,3,4,5,6,7,9,10},通常稱之為有序列,我們用序號1,2,3,…表示資料的位置,欲把一個新的資料8插入到上述序列中。

完成這個工作的步驟:
①確定資料“8”在原有序列中應該佔有的位置序號。資料“8”所處的位置應滿足小於或等於該位置右邊所有的資料,大於其左邊位置上所有的資料。

②將這個位置空出來,將資料“8”插進去。
直接插入排序(straight insertion sort)的做法是:
每次從無序表中取出第一個元素,把它插入到有序表的合適位置,使有序表仍然有序。
第一趟比較前兩個數,然後把第二個數按大小插入到有序表中; 第二趟把第三個資料與前兩個數從後向前掃描,把第三個數按大小插入到有序表中;依次進行下去,進行了(n-1)趟掃描以後就完成了整個排序過程。
直接插入排序是由兩層巢狀迴圈組成的。外層迴圈標識並決定待比較的數值。內層迴圈為待比較數值確定其最終位置。直接插入排序是將待比較的數值與它的前一個數值進行比較,所以外層迴圈是從第二個數值開始的。當前一數值比待比較數值大的情況下繼續迴圈比較,直到找到比待比較數值小的並將待比較數值置入其後一位置,結束該次迴圈。

#include<iostream>
using namespace std;
void Insertsort(int a[],int k)
{
    int i, j;
    for (i = 1;i < k;i++)//迴圈從第2個元素開始
    {
        if (a[i] < a[i - 1])
        {
            int temp = a[i];
            for (j = i - 1;j >= 0 && a[j] > temp;j--)
            {
                a[j + 1] = a[j];
            }
            a[j + 1] = temp;//此處就是a[j+1]=temp;
        }
    }
}
int main()
{
    int a[] = { 98,76,109,34,67,190,80,12,14,89,1 };
    int k = sizeof(a) / sizeof(a[0]);
    Insertsort(a,k);
    for (int f = 0;f < k;f++)
    {
        cout << a[f] << "  ";
    }
    return 0;
}

4、選擇排序

基本思想:第i趟排序從序列後n-i+1個元素中選擇一個最小的元素,與該n-i+1個元素的最前面那個元素進行位置交換,也就是與第i個位置上的元素進行交換,直道n=i-1;

直觀講,每一趟的選擇排序就是從序列中未排好順序的元素中選擇一個最小的元素,將鈣元素與這些未排好的元素中的第一個元素交換位置。

void Selectsort(int a[], int k)
{
    int i, j,min;
    int tmp;
    for (i = 0;i < k;i++)
    {
        min = i;
        for (j = i + 1;j < k;i++)
        {
            if (a[j] <= a[min])
            {
                min = j;
            }
        }
        if (min != i)      //如果找到比a[min]還要小的值就進行交換位置,否則不交換
        {
            tmp = a[min];
            a[min] = a[i];
            a[i] = tmp;
        }
    }
}

5、氣泡排序

基本思想描述:

1、將序列中的第一個元素和第二個元素進行比較,若前者大於後者,則將第一個元素與第二個元素進行位置交換,否則不交換

2、將第2個元素與第3個元素進行比較,同樣若前者大於後者,則將第2個元素與第3個元素進行位置交換,否則不交換。

3、以此類推,直到將第n-1個元素與第n個元素進行比較為止。此過程稱為第1趟氣泡排序,進過第21趟氣泡排序後,將長度為n的序列中最大的元素置於序列的尾部,即第n個位置上。

4、之後再進行第2趟…第n-1趟排序。氣泡排序完成。

改進思路:

以序列3 6 4 2 11 10 6為例:

第一趟bubblesort之後:

3 4 2 6 10 6 11

第二趟bubblesort之後:

3 2 4 6 6 10 11

第三趟bubblesort之後:

2 3 4 6 6 10 11

這時再進行氣泡排序就會發現,序列本身不會再發生變化,只有相鄰元素的比較,而沒有相鄰元素的交換,也就是說此時排序已經完成了。

所以可以這樣改進:

如果某一趟排序過程中只有元素之間的比較而沒有元素之間的位置交換,說明排序完成。

void ImprovedBubblesort(int a[], int n)
{
    int i, j;
    int tmp;
    int flag = 1;               //flag=1,說明本趟排序中仍有元素交換動作
    for (i = 0;i < n && flag==1;i++)       //趟次,一共n-1次
    {
        flag = 0;
        for (j = 0;j < n - 1;j++)  //元素交換
        {
            if (a[j] > a[i])
            {
                flag = 1;
                tmp = a[j];
                a[j] = a[i];
                a[i] = tmp;
            }
        }
    }
}

6、希爾排序

基本思路:

1、設定一個元素間隔增量gap,將參加排序的序列按照這個間隔數gap從第1個元素開始依次分成若干個子序列。

2、在子序列中可以採用其他的排序方法,例如氣泡排序。

3、縮小增量gap,重新將整個序列按照新的間隔數gap進行劃分,再分別對每個子序列排序。過程描述:縮小增量gap–>劃分序列–>將子序列排序

4、直到間隔數gap=1為止。

希爾排序過程:
在這裡插入圖片描述
思考:如何確定間隔數gap?
數學上仍然是一個尚未解決的難題,但是經驗告訴我們一種比較常用且效果好的方法:
1、首先gap取值為序列長度的一半
2、後續排序過程中,後一趟排序的gap取值為前一趟排序gap的一半取值
演算法描述:

void Shellsort(int a[], int n)
{
    int i, j;
    int tmp;
    int flag = 1;              
    int gap = n;            //第一次gap為n
    while (gap > 1)
    {
        //確定gap
        gap = gap / 2;
        //以gap劃分子序列,每一次迭代都是一組子序列的一趟自排序(冒泡)
        do {
                flag = 0;
                for (i = 0;i < n - gap;i++)
                {
                    j = i + gap;
                    if (a[j] < a[i])
                    {
                        flag = 1;
                        tmp = a[j];
                        a[j] = a[i];
                        a[i] = tmp;
                    }
                }
        } while (flag==1);
    }
}

7、快速排序

基本思想:
1、在當前的排序序列中任意選取一個元素,把該元素稱為基準元素或支點,把下雨等於基準元素的所有元素都移動到基準元素的前面,把大於基準元素的所有元素都移到基準元素的後面,這樣使得基準元素所處的位置 恰好就是排序的最終位置,並且把當前參加排序的序列分為前後兩個序列。
2、上述的過程稱為一趟快速排序,即快速排序的一次劃分
3、接下來分別對這兩個子序列重複上述的排序操作(如果子序列長度大於1的話),直到所有元素都被移動到排序後他們應處的最終位置上。
效率之所以高:每一次元素的移動都是跳躍的,不會像氣泡排序只能在相鄰元素之間進行,元素移動的間隔較大,因此總的比較和移動次數減少

具體步驟:

1、假設序列a,設定兩個變數i、j.分別指向首元素和尾元素,設定i指向的首元素為基準元素

2、反覆執行i++,直到i指向的元素>=基準元素,或者i指向尾部

3、反覆執行j–,直到指向的元素<基準元素,或者j指向頭部

4、若此時i<j,將i和j指向的元素進行交換。(大的元素在後面)

5、完成第一次交換後,重複執行步驟1、2,直到i>=j位置

6、此時i>=j,然後將基準元素與j指向的元素交換位置,至此完成了原序列的第一次劃分

7、接下來分別對基準元素前後的子序列中長度大於1的子序列重複執行上述操作。

步驟分析:

對於每個子序列的操作又是一次劃分,因此這個演算法具有遞迴性質。

每次劃分過程的基準元素仍可設定為子序列的第一個元素

//快速排序
void Quicksort(int a[], int s,int t)
{
    int i, j;
    if (s < t)
    {
        //【1】設定兩個變數i、j.分別指向首元素和尾元素,設定i指向的首元素為基準元素
        i = s;
        j = t + 1;
        while (1)
        {
            do i++;
            while(!(a[s]<=a[i] || i==t));               //【2】重複i++操作,直到i指向的元素>=基準元素,或者i指向尾部
            do j--;
            while (!(a[s]>=a[j] || j==s));              //【3】反覆執行j--,直到指向的元素<基準元素,或者j指向頭部
            if (i < j)                                  //【5】若此時i<j,將i和j指向的元素進行交換。(大的元素在後面)
            {
                swap(a[j], a[i]);
            }
            else break;                                  //【5】完成第一次交換後,重複執行步驟1、2,直到i>=j位置
        }
        //【6】此時i>=j,然後將基準元素與j指向的元素交換位置,至此完成了原序列的第一次劃分
        swap(a[s],a[j]);
        //【7】接下來分別對基準元素前後的子序列中長度大於1的子序列重複執行上述操作。
        Quicksort(a,s,j-1);                             //前半序列
        Quicksort(a,j+1,t);                             //後半序列

    }
}

快速排序只適用於順序表線性結構或者陣列序列的排序,不適合在連結串列上實現

8、堆排序

heapsort是選擇排序的改進。

首先了解一下堆的概念:

堆通常是一個可以被看做一棵完全二叉樹的陣列物件。

堆的定義如下:n個元素的序列{k1,k2,ki,…,kn}當且僅當滿足下關係時,稱之為堆。

(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2)

堆總是滿足下列性質:

1、 堆中某個節點的值總是不大於或不小於其父節點的值;

2、 堆總是一棵完全二叉樹。

瞭解一下完全二叉樹的概念:

https://blog.csdn.net/judgejames/article/details/87868602

將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆

舉例:

序列{49,22,40,20,18,36,6,12,17}
在這裡插入圖片描述
基於大頂堆的完全二叉樹表示的堆排序的核心思想可描述如下:

1、將原始序列構成一個堆(建立初始堆)

2、交換堆的第一個元素和堆的最後一個元素

3、將交換最大值元素之後的剩餘元素所構成的序列再轉換成一個堆

4、重複上述2、3步驟n-1次

經過上述操作,就可以將一個無序的序列從小到大進行排序。

關鍵問題:

1、如何原始序列構成一個堆

2、如何將交換最大值元素之後的剩餘元素所構成的序列再轉換成一個堆

第二個問題:

該二叉樹雖然不是一個堆,但是除了根結點外,其餘任何一棵子樹仍然滿足堆的特性。

自上而下調整:將序號為i的結點與其左右孩子(2i 、2i+1)三個值中的最大值替換到序號為i的結點的位置上。

只要徹底地完成一次自上而下的調整,該二叉樹就會變成一個堆。

最後一步:交換第一個元素和新堆的最後一個元素的位置,也就是將最大的元素移至新堆的最後。

第一個問題:

如果原序列對應的完全二叉樹有n個結點,則:

1、初始化時,令序列號為i= Floor (n/2),它對應於而擦函式中第 Floor (n/2)個結點(二叉樹中的結點按照層次編號,從1開始,從左到右,從上到下編號)

2、呼叫函式adjust調整

3、每執行完一次調整都執行一次i=i-1操作

4、重複步驟2、3,直到i==1時執行步驟5

5、最後再呼叫adjust函式調整一次。

這樣就完成調整了。

示例:初始序列為:{23,6,77,2,60,10,58,16,48,20}
在這裡插入圖片描述
code:

//堆排序
//【1】將二叉樹調整為一個堆的函式:
//函式作用:將以第i個元素作為根結點的子樹調整為一個新的堆序列,前提是該子樹中除了根結點外其餘的任何一個子樹仍然滿足堆的特性,如果該子樹除了根結點外其他子樹也不完全是
//堆結構的話,則不能僅通過依次呼叫adjust函式就將其調整為堆
//輸入:序列a   i:序列a中的元素下標
void BiTreeAdjustToHeap(int a[],int i,int n)
{
    int j;
    int tmp;
    tmp = a[i];
    j = 2 * i;      //j為i的左孩子結點序號
    while (j <= n)
    {
        if (j < n && a[j] < a[j + 1])
        {
            j++;                //j為i的左右孩子中較大孩子的序號
        }
        if (tmp >= a[j])        //如果父結點值比孩子值還大就不需要調整了
        {
            break;
        }
        a[j / 2] = a[j];       //較大的子節點與父節點交換位置
        j = 2 * j;             //繼續向下調整
    }
    a[j / 2] = tmp;
}

//【2】原始序列初始化函式
void InitHeap(int a[],int n)
{
    for(int i=n/2;i>=0;i--)
    {
        BiTreeAdjustToHeap(a,i,n);
    }
}
//堆排序函式
void Heapsort(int a[], int n)
{
    int i = 0;
    //【1】原始序列初始化函式
    InitHeap(a,n);
    //【2】交換第1個和第n個元素,再將根結點向下調整
    for (i = n - 1;i >= 0;i--)
    {
        swap(a[i+1],a[0]);
        BiTreeAdjustToHeap(a,0,i);      //將根結點向下調整
    }
}

需要把握的要點:

1、堆排序是針對線性序列的排序,之所以要採用完全二叉樹的形式解釋堆排序的過程,是出於方便解釋的需要

2、堆排序的第一步是將原序列變成一個對序列

3、一系列的交換調整操作。所謂交換就是將堆中第一個元素與本次調整範圍內的新堆的最後一個元素交換位置,使得較大的元素能夠置於序列的最後面,所謂調整就是將交換後的剩餘元素從上至下調整為為一個新堆的過程。

4、通過2、3操作可以將一個無序序列從小到大偶愛徐

5、如果基於大頂堆進行堆排序,則排序後的序列從小到大。若是基於小頂堆,則從大到小。

9、排序演算法效能比較

排序演算法平均時間最壞情況空間需求
直接插入排序O(n^2)O(n^2)O(1)
氣泡排序O(n^2)O(n^2)O(1)
簡單選擇排序O(n^2)O(n^2)O(1)
希爾排序O(nlog2n)O(nlog2n)O(1)
快速排序O(nlog2n)O(n^2)O(nlog2n)
堆排序O(nlog2n)O(nlog2n)O(1)

總結:

1、如果參加排序的序列最開始就是基本有序或者區域性有序的,使用這直接插入排序和氣泡排序的效果較好,排序速度較快,最好的情況下(原序列按值有序),時間複雜度O(n)

2、快速排序最快,堆排序空間消耗最小

3、序列中元素個數越小,採用氣泡排序排序演算法、直接插入排序、簡單選擇排序較合適

當序列規模變大時,採用希爾排序、快速排序和堆排序比較合適

4、從穩定性來講:直接插入、冒泡是穩定的排序方法。簡單選擇排序、希爾排序、快速排序、堆排序是不穩定的排序演算法

10、所有演算法的code(C語言)

#include<iostream>
using namespace std;
void swap(int& a, int& b)
{
    //方法一:   
    int tmp = 0;
    tmp = b;
    b = a;
    a = tmp;
    //方法二:   
    //a = a+b;   
    //b = a-b;   
    //a = a -b;   
    //方法三:   
    //a ^= b ^= a ^= b;   
    //方法四:    冒泡和希爾和改進冒泡,使用這個方法不成功         
    //a = a+b-(b=a);   
}
//參考:https://blog.csdn.net/shangguanyunlan/article/details/51778378         //
void Insertsort(int a[],int k)
{
    int i, j;
    for (i = 1;i < k;i++)//迴圈從第2個元素開始
    {
        if (a[i] < a[i - 1])
        {
            int temp = a[i];
            for (j = i - 1;j >= 0 && a[j] > temp;j--)
            {
                a[j + 1] = a[j];
            }
            a[j + 1] = temp;//此處就是a[j+1]=temp;
        }
    }
}
//選擇排序
void Selectsort(int a[], int k)
{
    int i, j,min;
    int tmp;
    for (i = 0;i < k;i++)
    {
        min = i;
        for (j = i + 1;j < k;i++)
        {
            if (a[j] <= a[min])
            {
                min = j;
            }
        }
        if (min != i)      //如果找到比a[min]還要小的值就進行交換位置,否則不交換
        {
            swap(a[min],a[i]);
        }
    }
}
//氣泡排序
void Bubblesort(int a[], int n)
{
    int i, j;
    int tmp;
    for (i=0;i<n;i++)       //趟次,一共n-1次
    {
        for (j = 0;j < n - 1;j++)  //元素交換
        {
            if (a[j] > a[i])
            {
                swap(a[j], a[i]);
            }
        }
    }
}
//改進的氣泡排序
void ImprovedBubblesort(int a[], int n)
{
    int i, j;
    int tmp;
    int flag = 1;               //flag=1,說明本趟排序中仍有元素交換動作
    for (i = 0;i < n && flag==1;i++)       //趟次,一共n-1次
    {
        flag = 0;
        for (j = 0;j < n - 1;j++)  //元素交換
        {
            if (a[j] > a[i])
            {
                flag = 1;
                swap(a[j], a[i]);
            }
        }
    }
}
//希爾排序
void Shellsort(int a[], int n)
{
    int i, j;
    int tmp;
    int flag = 1;              
    int gap = n;            //第一次gap為n
    while (gap > 1)
    {
        //確定gap
        gap = gap / 2;
        //以gap劃分子序列,每一次迭代都是一組子序列的一趟自排序(冒泡)
        do {
                flag = 0;
                for (i = 0;i < n - gap;i++)
                {
                    j = i + gap;
                    if (a[j] < a[i])
                    {
                        flag = 1;
                        swap(a[j], a[i]);
                    }
                }
        } while (flag==1);
    }
}
//快速排序
void Quicksort(int a[], int s,int t)
{
    int i, j;
    if (s < t)
    {
        //【1】設定兩個變數i、j.分別指向首元素和尾元素,設定i指向的首元素為基準元素
        i = s;
        j = t + 1;
        while (1)
        {
            do i++;
            while(!(a[s]<=a[i] || i==t));               //【2】重複i++操作,直到i指向的元素>=基準元素,或者i指向尾部
            do j--;
            while (!(a[s]>=a[j] || j==s));              //【3】反覆執行j--,直到指向的元素<基準元素,或者j指向頭部
            if (i < j)                                  //【5】若此時i<j,將i和j指向的元素進行交換。(大的元素在後面)
            {
                swap(a[j], a[i]);
            }
            else break;                                  //【5】完成第一次交換後,重複執行步驟1、2,直到i>=j位置
        }
        //【6】此時i>=j,然後將基準元素與j指向的元素交換位置,至此完成了原序列的第一次劃分
        swap(a[s],a[j]);
        //【7】接下來分別對基準元素前後的子序列中長度大於1的子序列重複執行上述操作。
        Quicksort(a,s,j-1);                             //前半序列
        Quicksort(a,j+1,t);                             //後半序列

    }
}


//堆排序
//【1】將二叉樹調整為一個堆的函式:
//函式作用:將以第i個元素作為根結點的子樹調整為一個新的堆序列,前提是該子樹中除了根結點外其餘的任何一個子樹仍然滿足堆的特性,如果該子樹除了根結點外其他子樹也不完全是
//堆結構的話,則不能僅通過依次呼叫adjust函式就將其調整為堆
//輸入:序列a   i:序列a中的元素下標
void BiTreeAdjustToHeap(int a[],int i,int n)
{
    int j;
    int tmp;
    tmp = a[i];
    j = 2 * i;      //j為i的左孩子結點序號
    while (j <= n)
    {
        if (j < n && a[j] < a[j + 1])
        {
            j++;                //j為i的左右孩子中較大孩子的序號
        }
        if (tmp >= a[j])        //如果父結點值比孩子值還大就不需要調整了
        {
            break;
        }
        a[j / 2] = a[j];       //較大的子節點與父節點交換位置
        j = 2 * j;             //繼續向下調整
    }
    a[j / 2] = tmp;
}

//【2】原始序列初始化函式
void InitHeap(int a[],int n)
{
    for(int i=n/2;i>=0;i--)
    {
        BiTreeAdjustToHeap(a,i,n);
    }
}
//堆排序函式
void Heapsort(int a[], int n)
{
    int i = 0;
    //【1】原始序列初始化函式
    InitHeap(a,n);
    //【2】交換第1個和第n個元素,再將根結點向下調整
    for (i = n - 1;i >= 0;i--)
    {
        swap(a[i+1],a[0]);
        BiTreeAdjustToHeap(a,0,i);      //將根結點向下調整
    }
}
void show_sort_result(int a[],int k)
{
    for (int f = 0;f < k;f++)
    {
        cout << a[f] << "  ";
    }
    printf("\n");
}
int main()
{
    int a[] = { 98,76,109,34,67,190,80,12,14,89,1 };
    int k = sizeof(a) / sizeof(a[0]);
    //printf("直接插入排序\n");
    //Insertsort(a,k);
    //show_sort_result(a,k);
    //printf("選擇排序\n");
    //Insertsort(a, k);
    //show_sort_result(a,k);
    //printf("氣泡排序\n");
    //Bubblesort(a, k);
    //show_sort_result(a, k);
    //printf("改進後的氣泡排序\n");
    //ImprovedBubblesort(a, k);
    //show_sort_result(a, k);
    //printf("希爾排序\n");
    //Shellsort(a, k);
    //show_sort_result(a, k);
    //printf("快速排序\n");
    //Quicksort(a,0,k-1);
    //show_sort_result(a, k);
    //printf("堆排序\n");
    //Heapsort(a,k-1);
    //show_sort_result(a, k);
    return 0;
}

相關文章