1. 程式人生 > >資料結構 基礎排序演算法

資料結構 基礎排序演算法

排序 研究


排序

順序表

將要排序的資料存入順序表

typedef struct MyStruct
{
	int key;
    ……
}ElemType;

typedef struct
{
	ElemType *elem;
	int length;
	int size;
	int increment;
} SqList;

這是我們接下來要討論的排序問題中的被排序的物件。

內部排序與外部排序

排序從被排序的物件在計算機裡的儲存位置分,內部排序就是待排序列全部儲存在記憶體中
外部排序是對大檔案的排序,由於檔案過大無法一次把全部放進記憶體,所以在排序過程中,需要記憶體與外部儲存器(硬碟)做多次資料交換。
我們就下來討論的主要是內部排序。

一趟排序
從無序區取出一個元素,按演算法策略加入到有序區中,即為一趟排序

排序策略

根據排序過程中所用策略不同,可將內部排序分成五大類:

  1. 交換排序
  2. 選擇排序
  3. 插入排序
  4. 歸併排序
  5. 基數排序

交換排序

最典型的兩種方法:

氣泡排序

氣泡排序的實現
基本思想:很簡單,就是依次遍歷順序表的所有元素,只要逆序就交換,直至全部有序。

//氣泡排序
void BubbleSort(puke* p, int n)
{
	puke t;
	for (int i = 0; i < n - 1; i++)
	{
		for (int j = 1; j < n-i; j++)
		{
			if (p[j].value < p[j-1].value)
			{
				t = p[j-1];
				p[j-1] = p[j];
				p[j] = t;
			}
		}
		//out(p, 5);
	}
}

效率分析:

時間複雜度: O ( n 2 ) O(n^2)
最好情況下,無需排序

最壞情況下, 3 2 n ( n 1 ) \frac{3}{2}n(n-1)

空間複雜度: O ( 1 ) O(1)

穩定性:穩定

快速排序:

是對氣泡排序的改進

基本思想
先從待排序列中選定一個“樞軸”(一般選待排序列的第一個元素),通過待排序列其他元素的關鍵字與樞軸的比較將待排序列劃分成大於和小於樞軸的兩個序列。在依次對這兩個序列遞迴上面的操作,最終使序列有序。

實現程式碼如下:
排序物件:

typedef int KeyType;

typedef struct RcdType
{
	KeyType key;
}RcdType;

函式實現:

//一次劃分函式
int Part(RcdType rcd[], int low, int high)
{
	rcd[0] = rcd[low];
	while (low<high)
	{
		while (low < high && rcd[high].key >= rcd[0].key)
			high--;

		rcd[low] = rcd[high];

		while (low < high && rcd[low].key <= rcd[0].key)
			low++;

		rcd[high] = rcd[low];
	}
	//此時low即為劃分後樞軸應該在的位置
	rcd[low] = rcd[0];
	return low;
}


void QSort(RcdType rcd[], int s, int t)
{
	int mid;
	//樞軸位置
	if (s <= 0 || t <= 0)
		return;
	else if (s<t)
	{
		mid = Part(rcd, s, t);
		QSort(rcd, mid+1, t);
		QSort(rcd, s, mid-1);
	}
}

注意:

  1. 遞迴呼叫的時候,劃分前後區域要注意樞軸位置加一或減一
  2. 這裡為了方便用順序表的第0個元素來存放樞軸,所以呼叫的時候要注意下標是否合理

效率分析

時間複雜度: O ( n 2 ) O(n^2)
理想情況下: O ( n l o g 2 ( n ) ) O(nlog_2(n))
空間複雜度: O ( n ) O(n)

穩定性:不穩定

快速排序的測試

選擇排序

簡單選擇排序:

基本思想
很簡單,就是每遍歷一次待排序列,就找出最小的記錄放到序列最前面,然後在其後面重復該操作,又或者是每一趟遍歷都找出最大的記錄放到最後面,然後在其前面重複該操作。

//c/c++實現
void SelectionSort(int*a ,int n)
{
	int t,k,min;
	for (int  i = 0; i < n-1; i++)
	{
		min = i;
		for (int j = i; j < n; j++)
		{
			if (a[j]<a[min])
				min = j;
		}
		t = a[i];
		a[i] = a[min];
		a[min] = t;
		out(a, n);
	}
}

java:

 public static void SelectionSort(int a[],int n)
    {
        int i,t,min;
        for (i = 0;i < n - 1;i++)
        {
            min = i;
            for (int j = i;j < n;j++)
                if (a[j] < a[min])
                    min = j;
            t = a[i];
            a[i] = a[min];
            a[min] = t;
            out(a,n);
        }
    }

python

def SelectionSort(s,n):
    for i in range(0,n-1):
        min = i
        for j in range(i,n):
            if a[j] < a[min]:
                min = j
        t = a[i]
        a[i] = a[min]
        a[min] = t
        out(s,n)

效率分析

時間複雜度: O ( n 2 ) O(n^2)

空間複雜度: O ( 1 ) O(1)

穩定性:不穩定

堆排序:

基本思想:涉及到二叉樹的知識,藉助了大小頂堆來進行進一步排序,最終得到以層次遍歷出來的元素是有序的。以大頂堆為例,先把堆頂結點與堆尾節點交換,然後長度減一(即最大的元素已到位)然後對餘下的結點進行堆調整,便可得到次大節點,重複下去即可得到升序序列。

效率分析

時間複雜度: O ( n × l o g 2 ( n ) ) O(n \times log_2(n))
穩定性:是不穩定的

插入排序

直接插入排序

基本思想:
類似於打撲克牌,每次從後面拿出一張,按照順序,插入到前面的排好的序列中去

虛擬碼:

insertionSort(A,n)
    for i = 1 to n-1
        v = A[i]
        j = i - 1
        while j >= 0 && A[j] > v
            A[j+1] = A[j]
            j = j-1
            //cnt++ 單純地計數,用來估計時間效率
        A[j+1] = v

程式碼片段一:

void InsertSort(SqList &L)
{
	int i, j;
	for (i = 0; i < L.length; i++)
	{
		if (L.elem[i+1].key<L.elem[i].key)
		{
			L.elem[0] = L.elem[i + 1];
			j = i + 1;
			do
			{
				j--;
				L.elem[j + 1] = L.elem[j];
			} while (L.elem[0].key < L.elem[j-1].key);
			L.elem[j] = L.elem[0];
		}
	}
}

程式碼片段二:

//直接插入排序
void insertSort(int*a ,int n)
{
	int j,t,k;
	for (int  i = 1; i < n; i++)
	{
		t = a[i];
		j = i - 1;
		while (j>=0 && a[j]>t)
		{
			a[j + 1] = a[j];
			j--;
		}
		a[j+1] = t;
		out(a, n);
	}
}

java:

public static void insertSort(int a[],int n)
    {
        int i,j,t;
        for (i=1;i<n;i++)
        {
            j = i-1;
            t = a[i];
            while (j>=0 && t<a[j])
            {
                a[j+1] = a[j];
                j--;
            }
            a[j+1] = t;
            out(a,n);
        }
    }

python:

def insertSort(s,n):
    for i in range(1,n):
        t = s[i]
        j = i-1
        while j >= 0 and s[j] > t:
            s[j + 1] = s[j]
            j -= 1
        s[j + 1] = t;
        out(s,n)

效率分析

空間效率:只需一個輔助空間,複雜度為 O ( 1 ) O(1)

時間效率:
最好情況下,關鍵字比較次數為n-1

最壞情況下,時間複雜度為 O ( n 2 ) O(n^2)

穩定性:是穩定的

希爾排序

是對直接插入排序的改進,又稱之為縮小增量排序。
將排序序列按增量d劃分成d個子序列,不斷減小增量d直到d為1。每次劃分都分別對每個子序列用直接插入法排序

虛擬碼:

//間隔為g的插入排序:
insertionSort(A,n,g)
    for i = g to n-1
        v = A[i]
        j = i - g
        while j >= 0 && A[j] > v
            A[j+g] = A[j]
            j = j-g
            cnt++
        A[j+g] = v

//希爾排序其實就是上面的迴圈,迴圈過程中,g以一個合適的方式遞減
ShellSort(A,n)
    cnt = 0
    m = ?
    G[] = {?}
    for i = 0 to m - 1
        insertionSort(A,n,G[i])

希爾排序中用到的插入排序與插入排序稍有不同。
希爾當中是一趟排序,根據間隔把數提出來排好序後再按照間隔插回去。

程式碼實現(《資料結構》 教材 廣工版)

//希爾排序:一趟排序+排序函式
void ShellInsert(SqList &L, int dk)
{
	//一趟希爾排序
	int i, j;
	for ( i = 0; i < L.length-dk; i++)
	{
		if (L.elem[i+dk].key < L.elem[i].key)
		{
			L.elem[0] = L.elem[i + dk];
			j = i + dk;
			do
			{
				j -= dk;
				L.elem[j + dk] = L.elem[j];

			} while (j-dk>0 && L.elem[0].key<L.elem[j-dk].key);
			L.elem[j] = L.elem[0];
		}
	}
}

void ShellSort(SqList &L,int d[],int t)
{
	//增量序列d:0到t-1
	int k;
	for ( k = 0; k < t; k++)
	{
		ShellInsert(L, d[k]);
	}
}

效率評價:

時間分析是一個複雜問題,所以沒有一個好的解決方法

間隔數列的選取:
g = 1,4,13,40,121……
g n + 1 = 3 g n + 1 g_{n+1} = 3g_n + 1

時間複雜度:
通過以上選取方法,基本可使複雜度穩定在 O ( n 1.25 ) O(n^{1.25}) 左右

穩定性:不穩定

歸併排序

基本思想:把待排序列遞迴分解成若干個長度大致相等的有序子序列,而後合併成一個有序序列。

一般採用兩兩分解和歸併的策略,這樣的歸併稱之為2路歸併排序演算法

演算法的主要實現:

/*歸併排序(2路)*/
//2路歸併演算法
void Merge(RcdType R1[], RcdType R2[], int i, int m, int n)
{
	int k, j;
	for (j = m + 1, k = i; i <= m && j <= n; k++)
	{
		if (R1[i].key <= R1[j].key)
			R2[k] = R1[i++];
		else
			R2[k] = R1[j++];
	}
	while (i <= m)
		R2[k++] = R1[i++];
	while (j <= n)
		R2[k++] = R1[j++];
}

//遞迴歸併
void MSort(RcdType R1[], RcdType R2[], int i, int s, int t)
{
	int m;
	if (s == t)
	{
		if (i % 2 == 1)
			R2[s] = R1[s];
	}
	else
	{
		m = (s + t) / 2;
		MSort(R1, R2, i + 1, s, m);
		MSort(R1, R2, i + 1, m + 1, t);
		if (i % 2 == 1)
			Merge(R1, R2, s, m, t);
		else
			Merge(R2, R1, s, m, t);
	}
}


void MergeSort(RcdSqList &L)
{
	RcdType *R;
	R = (RcdType*)malloc((L.length + 1) * sizeof(RcdType));
	MSort(L.rcd, R, 0, 0, L.length - 1);
	free(R);
}

注意事項:
在MergeSort函式中呼叫的MSort函式中的引數注意不要輸錯了,《資料結構(廣工版)》上是

MSort(L.rcd, R, 0, 1, L.length);

但是我這裡的陣列下標是從0到L.length-1,所以應該改為:

MSort(L.rcd, R, 0, 0, L.length - 1);

效率分析

時間複雜度: O ( n l o g 2 ( n ) ) O(nlog_2(n))

空間複雜度: O ( n ) O(n)

穩定性:穩定

基數排序

計數基數排序

//計數基數排序
typedef struct MyStruct
{
	int *keys;

}KElemType;

typedef struct
{
	KElemType *elem;
	int length;		//順序表長度
	int size;		//順序表容量
	int digitNum;	//關鍵字位數
	int radix;		//關鍵字基數

}KSqList;

void RadixPass(KElemType rcd[],KElemType rcd1[],int n,int i,int count[],int pos[],int radix)
{
	int k, j;
	for (k = 1; k <= n; k++)
		count[rcd[k].keys[i]]++;
	pos[0] = 1;
	for (j = 1; j <= radix; k++)
		pos[j] = count[j - 1] + pos[j - 1];
	for ( k = 0; k <= n; k++)
	{
		j = rcd[k].keys[i];
		rcd1[pos[j]++] = rcd[k];
	}
}

Status RadixSort(KSqList &L)
{
	KElemType *rcdl;
	int i = 0,j;
	int *count, *pos;
	count = (int*)malloc(L.radix*sizeof(int));
	pos = (int *)malloc(L.radix*sizeof(int));
	rcdl = (KElemType*)malloc((L.length+1)*sizeof(KElemType));

	if (NULL == count || NULL == pos || NULL == rcdl)
		return OVERFLOW;
	while (i < L.digitNum)
	{
		for (j = 0; j < L.radix; j++) count[j] = 0;
		if (0 == i % 2)
			RadixPass(L.elem, rcdl, L.length, i++, count, pos, L.radix);
		else
			RadixPass(rcdl,L.elem,L.length,i++,count,pos,L.radix);
	}

	if (1 == L.digitNum % 2)
		for (j = 1; j <= L.length; j++)
			L.elem[j] = rcdl[j];
	free(count);
	free(pos);
	free(rcdl);
	return OK;
 }

一趟收集和分配的時間複雜度為 O ( n ) O(n)

如果有m個關鍵字,則需要m趟,時間複雜度為 O ( m n ) O(mn)
一般m遠小於n, 故時間複雜度可看作 O ( n ) O(n)

如何判斷排序演算法是穩定的

什麼是穩定的排序演算法?
當出現“鍵值”相同的多個元素時,排序後,這些元素的順序與輸入時是一致的

示例一:氣泡排序與選擇排序

#include <iostream>

using namespace std;

class puke
{
public:
	puke(char suit, int value);
	puke();
	~puke();

	char suit;
	int value;
private:

};

puke::puke(char suit, int value)
{
	this->suit = suit;
	this->value = value;
}

puke::puke()
{
}

puke::~puke()
{
}

//為方便後面的穩定性的比較,需要過載運算子“==”
bool operator==(const puke& a,const puke& b)
{
	if (a.value == b.value && a.suit == b.suit)
	{
		return true;
	}
	return false;
}

//輸出函式
void out(puke* p,int n)
{
	for (int i = 0; i < n; i++)
		cout << p[i].suit << p[i].value << " ";
	cout << endl;

}

//氣泡排序
void BubbleSort(puke* p, int n)
{
	puke t;
	for (int i = 0; i < n - 1; i++)
	{
		for (int j = 1; j < n-i; j++)
		{
			if (p[j].value < p[j-1].value)
			{
				t = p[j-1];
				p[j-1] = p[j];
				p[j] = t;
			}
		}
		//out(p, 5);
	}
}

//選擇排序
void Selection(puke* p, int n)
{
	puke t;
	int min;
	for (int i = 0; i < n-1; i++)
	{
		min = i;
		for (int j = i + 1; j < n; j++)
		{
			if (p[j].value < p[min].value)
				min = j;
		}
		t = p[min];
		p[min] = p[i];
		p[i] = t;
		out(p, n);
	}
	out(p, n);
}

//用笨辦法(暴力方法)判斷演算法的穩定性
int isStable(puke* in,puke* out,int n)
{
	for (int i = 0; i < n - 1; i++)
		for (int j = i+1; j < n-1; j++)
			for (int a = 0;a<n-1;a++)
				for (int b = a+1; b < n-1; b++)
				{
					if (in[i].value == in[j].value && in[i] == out[b] && in[j] == out[a])
					{
						printf("No Stable.\n");
						return 0;
					}
				}
	printf("Stable.\n");
	return 1;
}

puke s1[] = { {'H',4},{'C',9},{'S',4},{'D',2},{'C',3} };
puke s2[] = { { 'H',4 },{ 'C',9 },{ 'S',4 },{ 'D',2 },{ 'C',3 } };

int main()
{
	return 0;
}