浅谈十大排序算法

十种常见内存内部排序算法一般分为以下几种: 
(1)非线性时间比较类排序:交换类排序(快速排序和冒泡排序)、插入类排序(简单插入排序和希尔排序)、选择类排序(简单选择排序和堆排序)、归并排序(二路归并排序和多路归并排序);

(2)线性时间非比较类排序:计数排序、基数排序和桶排序。

总结: 
(1)在比较类排序中,归并排序号称最快,其次是快速排序和堆排序,两者不相伯仲,但是有一点需要注意,数据初始排序状态对堆排序不会产生太大的影响,而快速排序却恰恰相反。

(2)线性时间非比较类排序一般要优于非线性时间比较类排序,但前者对待排序元素的要求较为严格,比如计数排序要求待排序数的最大值不能太大,桶排序要求元素按照hash分桶后桶内元素的数量要均匀。线性时间非比较类排序的典型特点是以空间换时间。

直接插入排序

原理:从待排序的n个记录中的第二个记录开始,依次与前面的记录比较并寻找插入的位置,每次外循环结束后,将当前的数插入到合适的位置。

稳定性:稳定排序。

时间复杂度: O(n)至O(n2),平均时间复杂度是O(n2)。

最好情况:当待排序记录已经有序,这时需要比较的次数是Cmin=n−1=O(n)。

最坏情况:如果待排序记录为逆序,则最多的比较次数为Cmax=∑i=1n−1(i)=n(n−1)2=O(n2)。

//STL 迭代器的方式实现
	template<typename iterator>
	void insertionsort(const iterator &begin, const iterator &end);

	template<typename iterator,typename comparator>
	void insertionsort(const iterator &begin, const iterator &end, comparator lessthan);

    template<typename iterator>
void insertionsort(const iterator &begin, const iterator &end)
{
	insertionsort(begin, end, less<decltype(*begin)>());
}

template<typename iterator, typename comparator>
void insertionsort(const iterator &begin, const iterator &end, comparator lessthan)
{
	if (begin == end)
	{
		return;
	}
	iterator j;
	for (iterator p = begin + 1; p != end; ++p)
	{
		auto tmp = std::move(*p);
		for (j = p; j != begin && lessthan(tmp, *(j - 1)); --j)
		{
			*j = std::move(*(j - 1));
		}
		*j = std::move(tmp);

	}
}

//调用
int ib[9] = { 0,1,2,3,4,8,9,3,5 };
vector<int> ivec2(ib, ib + 9);
insertionsort<vector<int>::iterator>(ivec2.begin(), ivec2.end());
void insertSort(int A[], int len);

//A:输入数组,len:数组长度
void insertSort(int A[], int len)
{
	int temp;
	for (int i = 1; i < len; i++)
	{
		int j = i - 1;
		temp = A[i];
		//查找到要插入的位置
		while (j >= 0 && A[j] > temp)
		{
			A[j + 1] = A[j];
			j--;
		}
		if (j != i - 1)
			A[j + 1] = temp;
	}
}

//调用
int ib[9] = { 0,1,2,3,4,8,9,3,5 };
insertSort(ib, 9);

冒泡排序

算法思想: 
从数组中第一个数开始,依次遍历数组中的每一个数,通过相邻比较交换,每一轮循环下来找出剩余未排序数的中的最大数并”冒泡”至数列的顶端。

 算法步骤

1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。

2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

3.针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

稳定性:稳定排序。

时间复杂度: O(n)至O(n2),平均时间复杂度为O(n2)。

最好的情况:如果待排序数据序列为正序,则一趟冒泡就可完成排序,排序码的比较次数为n-1次,且没有移动,时间复杂度为O(n)。

最坏的情况:如果待排序数据序列为逆序,则冒泡排序需要n-1次趟起泡,每趟进行n-i次排序码的比较和移动,即比较和移动次数均达到最大值: 
比较次数:Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2) 
移动次数等于比较次数,因此最坏时间复杂度为O(n2)。

//冒泡排序   //整数或浮点数皆可使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符
template<typename T> 
void bubble_sort(T arr[], int len);

template<typename T>
void bubble_sort(T arr[], int len)
{
	int i, j;
	for (i = 0; i < len - 1; i++)
		for (j = 0; j < len - 1 - i; j++)
			if (arr[j] > arr[j + 1])
				swap(arr[j], arr[j + 1]);
}
//调用
	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	bubble_sort(arr, len);

简单选择排序(又称直接选择排序)

原理:从所有记录中选出最小的一个数据元素与第一个位置的记录交换;然后在剩下的记录当中再找最小的与第二个位置的记录交换,循环到只剩下最后一个数据元素为止。

稳定性:不稳定排序。

时间复杂度: 最坏、最好和平均复杂度均为O(n2),因此,简单选择排序也是常见排序算法中性能最差的排序算法。简单选择排序的比较次数与文件的初始状态没有关系,在第i趟排序中选出最小排序码的记录,需要做n-i次比较,因此总的比较次数是:∑i=1n−1(n−i)=n(n−1)/2=O(n2)。

//直接选择排序
template<typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定大於(>)的運算子功能
void selection_sort(T arr[], int len);

template<typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定大於(>)的運算子功能
void selection_sort(T arr[], int len)
{
	int i, j;
	for (i = 0; i < len - 1; i++)
	{
		int min = i;
		for (j = i + 1; j < len; j++)     //走訪未排序的元素
			if (arr[j] < arr[min])    //找到目前最小值
				min = j;    //紀錄最小值
		swap(arr[min], arr[i]);    //做交換
	}
}

	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	selection_sort(arr, len);

希尔排序

Shell 排序又称缩小增量排序, 由D. L. Shell在1959年提出,是对直接插入排序的改进。

原理: Shell排序法是对相邻指定距离(称为增量)的元素进行比较,并不断把增量缩小至1,完成排序。

步骤:

选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;

按增量序列个数 k,对序列进行 k 趟排序;

每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

在直接插入排序的基础上,将直接插入排序中的1全部改变成增量d即可,因为Shell排序最后一轮的增量d就为1。

稳定性:不稳定排序。

时间复杂度:O(n1.3)到O(n2)。Shell排序算法的时间复杂度分析比较复杂,实际所需的时间取决于各次排序时增量的个数和增量的取值。研究证明,若增量的取值比较合理,Shell排序算法的时间复杂度约为O(n1.3)。

对于增量的选择,Shell 最初建议增量选择为n/2,并且对增量取半直到 1;D. Knuth教授建议di+1=⌊di−13⌋序列。


//希尔排序
template<typename T>
void shell_sort(T array[], int length);

template<typename T>
void shell_sort(T array[], int length)
{
	for (int gap = length/2; gap > 0; gap/= 2)
	{
		for (int i = gap; i < length ; ++i)
		{
			T tmp = std::move(array[i]);
			int j = i;
			for (;j >= gap && tmp < array[j - gap]; j-=gap)
			{
				array[j] = std::move(array[j - gap]);
			}
			array[j] = std::move(tmp);
		}
	}
}

	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	shell_sort(arr, len);

堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)

1. 算法步骤

  1. 创建一个堆 H[0……n-1];

  2. 把堆首(最大值)和堆尾互换;

  3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;

  4. 重复步骤 2,直到堆的尺寸为 1。

//希尔排序
template<typename T>
void heap_sort(T arr[], int len);

template<typename T>
void max_heapify(T arr[], int start, int end);

template<typename T>
void max_heapify(T arr[], int start, int end)
{
	// 建立父節點指標和子節點指標
	int dad = start;
	int son = dad * 2 + 1;
	while (son <= end) { // 若子節點指標在範圍內才做比較
		if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的
			son++;
		if (arr[dad] > arr[son]) // 如果父節點大於子節點代表調整完畢,直接跳出函數
			return;
		else { // 否則交換父子內容再繼續子節點和孫節點比較
			swap(arr[dad], arr[son]);
			dad = son;
			son = dad * 2 + 1;
		}
	}
}


template<typename T>
void heap_sort(T arr[], int len)
{
	// 初始化,i從最後一個父節點開始調整
	for (int i = len / 2 - 1; i >= 0; i--)
		max_heapify(arr, i, len - 1);
	// 先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢
	for (int i = len - 1; i > 0; i--) {
		swap(arr[0], arr[i]);
		max_heapify(arr, 0, i - 1);
	}
}



	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	heap_sort<int>(arr, len);


归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

稳定性:稳定排序算法;

时间复杂度: 最坏,最好和平均时间复杂度都是Θ(nlgn)

 算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;

  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

  4. 重复步骤 3 直到某一指针达到序列尾;

  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

//归并
template<typename T>
void merge_sort(T arr[],int len);


template<typename T>
void mergesort(T arr[], T tmparr[], int left, int right);


template<typename T>
void merge(T arr[], T tmparr[], int leftpos, int rightpos, int rightend);


template<typename T>
void merge_sort(T arr[], int len)
{
	T *tmparr = new T[len];
	mergesort(arr, tmparr, 0, len - 1);
	if (tmparr)
	{
		delete tmparr;
	}
}

template<typename T>
void mergesort(T arr[], T tmparr[], int left, int right)
{
	if (left < right)
	{
		int center = (left + right) / 2;
		mergesort(arr, tmparr, left, center);
		mergesort(arr, tmparr, center + 1, right);
		merge(arr, tmparr, left, center + 1, right);

	}
}

template<typename T>
void merge(T arr[], T tmparr[], int leftpos, int rightpos, int rightend)
{
	int leftend = rightpos - 1;
	int tmppos = leftpos;
	int numelements = rightend - leftpos + 1;

	while (leftpos <= leftend && rightpos <= rightend)
	{
		if (arr[leftpos] <= arr[rightpos])
		{
			tmparr[tmppos++] = std::move(arr[leftpos++]);
		}
		else
		{
			tmparr[tmppos++] = std::move(arr[rightpos++]);
		}
	}

	while (leftpos <= leftend)
	{
		tmparr[tmppos++] = std::move(arr[leftpos++]);
	}
	while (rightpos <= rightend)
	{
		tmparr[tmppos++] = std::move(arr[rightpos++]);
	}

	for (int i = 0; i < numelements; ++i,--rightend)
	{
		arr[rightend] = std::move(tmparr[rightend]);
	}


}

	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	merge_sort<int>(arr, len);

快速排序

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

算法原理: 
(1)从待排序的n个记录中任意选取一个记录(通常选取第一个记录)为分区标准;

(2)把所有小于该排序列的记录移动到左边,把所有大于该排序码的记录移动到右边,中间放所选记录,称之为第一趟排序;

(3)然后对前后两个子序列分别重复上述过程,直到所有记录都排好序。

稳定性:不稳定排序。

时间复杂度: O(nlog2n)至O(n2),平均时间复杂度为O(nlgn)

最好的情况:是每趟排序结束后,每次划分使两个子文件的长度大致相等,时间复杂度为O(nlog2n)。

最坏的情况:是待排序记录已经排好序,第一趟经过n-1次比较后第一个记录保持位置不变,并得到一个n-1个元素的子记录;第二趟经过n-2次比较,将第二个记录定位在原来的位置上,并得到一个包括n-2个记录的子文件,依次类推,这样总的比较次数是: 

Cmax=∑i=1n−1(n−i)=n(n−1)/2=O(n2)

template<typename T>
void quickSort(T a[], int low, int high);

template<typename T>
void quickSort(T a[], int low, int high)
{
	if (low >= high)
	{
		return;
	}
	int left = low;
	int right = high;
	T key = a[left];    /*用数组的第一个记录作为分区元素*/
	while (left != right) {
		while (left < right&&a[right] >= key)    /*从右向左扫描,找第一个码值小于key的记录,并交换到key*/
			--right;
		a[left] = a[right];
		while (left < right&&a[left] <= key)
			++left;
		a[right] = a[left];    /*从左向右扫描,找第一个码值大于key的记录,并交换到右边*/
	}
	a[left] = key;    /*分区元素放到正确位置*/
	quickSort(a, low, left - 1);
	quickSort(a, left + 1, high);

}

	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	quickSort(arr, 0, len -1);

计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。它的优势在于在对于较小范围内的整数排序。它的复杂度为Ο(n+k)(其中k是待排序数的范围),快于任何比较排序算法,缺点就是非常消耗空间。很明显,如果而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序,比如堆排序和归并排序和快速排序。

算法原理: 
基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。

算法步骤: 
(1)找出待排序的数组中最大的元素; 
(2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项; 
(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加); 
(4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

时间复杂度:Ο(n+k)。

空间复杂度:Ο(k)。

要求:待排序数中最大数值不能太大。

稳定性:稳定。

void countSort(int *arr, int *sorted_arr, int n);

#define MAXNUM 20    //待排序数的最大个数

#define MAX    100   //待排序数的最大值


//arr:待排序数组,sorted_arr:排好序的数组,n:待排序数组长度
void countSort(int *arr, int *sorted_arr, int n)  
{   
    int i;   
    int *count_arr = (int *)malloc(sizeof(int) * (MAX+1));  
 
    //初始化计数数组   
    memset(count_arr,0,sizeof(int) * (MAX+1));
 
    //统计i的次数   
    for(i = 0;i<n;i++)  
        count_arr[arr[i]]++;  
    //对所有的计数累加,作用是统计arr数组值和小于小于arr数组值出现的个数
    for(i = 1; i<=MAX; i++)  
        count_arr[i] += count_arr[i-1];   
    //逆向遍历源数组(保证稳定性),根据计数数组中对应的值填充到新的数组中   
    for(i = n-1; i>=0; i--)  
    {  
        //count_arr[arr[i]]表示arr数组中包括arr[i]和小于arr[i]的总数
        sorted_arr[count_arr[arr[i]]-1] = arr[i];  
 
        //如果arr数组中有相同的数,arr[i]的下标减一
        count_arr[arr[i]]--;    
    }
    free(count_arr);
}

	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	int sorted_arr[11] = { 0 };
	countSort(arr, sorted_arr,len);

桶排序

桶排序也是分配排序的一种,但其是基于比较排序的,这也是与基数排序最大的区别所在。

思想:桶排序算法想法类似于散列表。首先要假设待排序的元素输入符合某种均匀分布,例如数据均匀分布在[ 0,1)区间上,则可将此区间划分为10个小区间,称为桶,对散布到同一个桶中的元素再排序。

要求:待排序数长度一致。

排序过程: 
(1)设置一个定量的数组当作空桶子; 
(2)寻访序列,并且把记录一个一个放到对应的桶子去; 
(3)对每个不是空的桶子进行排序。 
(4)从不是空的桶子里把项目再放回原来的序列中。

时间复杂度: 
对N个关键字进行桶排序的时间复杂度分为两个部分: 
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,对于N个待排数据,M个桶,平均每个桶[N/M]个数据,则桶内排序的时间复杂度为 ∑i=1MO(Ni∗logNi)=O(N∗logNM) 。其中Ni 为第i个桶的数据量。

因此,平均时间复杂度为线性的O(N+C)C为桶内排序所花费的时间。当每个桶只有一个数,则最好的时间复杂度为:O(N)。


typedef struct node
{
	int keyNum;//桶中数的数量
	int key;   //存储的元素
	struct node * next;
}KeyNode;

//keys待排序数组,size数组长度,bucket_size桶的数量
void inc_sort(int keys[], int size, int bucket_size)
{
	KeyNode* k = (KeyNode *)malloc(sizeof(KeyNode)); //用于控制打印
	int i, j, b;
	KeyNode **bucket_table = (KeyNode **)malloc(bucket_size * sizeof(KeyNode *));
	for (i = 0; i < bucket_size; i++)
	{
		bucket_table[i] = (KeyNode *)malloc(sizeof(KeyNode));
		bucket_table[i]->keyNum = 0;//记录当前桶中是否有数据
		bucket_table[i]->key = 0;   //记录当前桶中的数据  
		bucket_table[i]->next = NULL;
	}

	for (j = 0; j < size; j++)
	{
		int index;
		KeyNode *p;
		KeyNode *node = (KeyNode *)malloc(sizeof(KeyNode));
		node->key = keys[j];
		node->next = NULL;

		index = keys[j] / bucket_size;        //映射函数计算桶号  
		p = bucket_table[index];   //初始化P成为桶中数据链表的头指针  
		if (p->keyNum == 0)//该桶中还没有数据 
		{
			bucket_table[index]->next = node;
			(bucket_table[index]->keyNum)++;  //桶的头结点记录桶内元素各数,此处加一
		}
		else//该桶中已有数据 
		{
			//链表结构的插入排序 
			while (p->next != NULL && p->next->key <= node->key)
				p = p->next;
			node->next = p->next;
			p->next = node;
			(bucket_table[index]->keyNum)++;
		}
	}
	//打印结果
	for (b = 0; b < bucket_size; b++)
		//判断条件是跳过桶的头结点,桶的下个节点为元素节点不为空
		for (k = bucket_table[b]; k->next != NULL; k = k->next)
		{
			printf("%d ", k->next->key);
			qDebug() << k->next->key;
		}
}

基数排序

基数排序属于“分配式排序”(distribution sort),是非比较类线性时间排序的一种,又称“桶子法”(bucket sort),顾名思义,它是透过键值的部分信息,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。是线性时间排序算法中性能最好的排序算法.

基数排序(以整形为例),将整形10进制按每位拆分,然后从低位到高位依次比较各个位。主要分为两个过程:
(1)分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中(比如64,个位为4,则放入4号桶中);
(2)收集,再将放置在0~9号桶中的数据按顺序放到数组中;
(3)重复(1)(2)过程,从个位到最高位(比如32位无符号整型最大数4294967296,最高位为第10位)。基数排序的方式可以采用LSD(Least Significant Digital)或MSD(Most Significant Digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。


int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
	int maxData = data[0];              ///< 最大数
	/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
	for (int i = 1; i < n; ++i)
	{
		if (maxData < data[i])
			maxData = data[i];
	}
	int d = 1;
	int p = 10;
	while (maxData >= p)
	{
		maxData /= 10;
		++d;
	}
	return d;

}
void radixsort(int data[], int n) //基数排序
{
	int d = maxbit(data, n);
	int *tmp = new int[n];
	int *count = new int[10]; //计数器
	int i, j, k;
	int radix = 1;
	for (i = 1; i <= d; i++) //进行d次排序
	{
		for (j = 0; j < 10; j++)
			count[j] = 0; //每次分配前清空计数器
		for (j = 0; j < n; j++)
		{
			k = (data[j] / radix) % 10; //统计每个桶中的记录数
			count[k]++;
		}
		for (j = 1; j < 10; j++)
			count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
		for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
		{
			k = (data[j] / radix) % 10;
			tmp[count[k] - 1] = data[j];
			count[k]--;
		}
		for (j = 0; j < n; j++) //将临时数组的内容复制到data中
			data[j] = tmp[j];
		radix = radix * 10;
	}
	delete[]tmp;
	delete[]count;
}



	int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	radixsort(arr, len);


 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值