数据结构 基础排序算法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34844814/article/details/85463058

排序 研究


顺序表

将要排序的数据存入顺序表

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;
}

猜你喜欢

转载自blog.csdn.net/qq_34844814/article/details/85463058