经典排序算法总结

概览

排序算法经过了很长时间的演变,产生了很多种不同的方法。对于初学者来说,对它们进行整理便于理解记忆显得很重要。每种算法都有它特定的使用场合,很难通用。因此,我们很有必要对所有常见的排序算法进行归纳。

一、直接插入排序

1、算法的伪代码(这样便于理解):    

     INSERTION-SORT (A, n)             A[1 . . n] 
     for j ←2 to n 
          do key ← A[ j] 
          i ← j – 1 
          while i > 0 and A[i] > key 
               do A[i+1] ← A[i] 
                    i ← i – 1 
          A[i+1] = key

2、思想:如下图所示,每次选择一个元素K插入到之前已排好序的部分A[1…i]中,插入过程中K依次由后向前与A[1…i]中的元素进行比较。若发现发现A[x]>=K,则将K插入到A[x]的后面,插入前需要移动元素。

image

3、算法时间复杂度。  
最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n) 
最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n^2)  
平均情况下:O(n­2)

4、稳定性。  
理解性记忆比死记硬背要好。因此,我们来分析下。稳定性,就是有两个相同的元素,排序先后的相对位置是否变化,主要用在排序时有多个排序规则的情况下。在插入排序中,K1是已排序部分中的元素,当K2和K1比较时,直接插到K1的后面(没有必要插到K1的前面,这样做还需要移动!!),因此,插入排序是稳定的。

5、代码实现

/*
对于一个int数组,请编写一个插入排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
*/
#include <iostream>
#include <cstdlib>
using namespace std;
/*直接插入排序*/
int *IntertionSort(int *A, int n)
{
	if (A == NULL || n <= 0)
		return A;
	/*将序列中的i=1~i=n-1的元素依次选择合适位置插入*/
	for (int i = 1; i < n; ++i)
	{
		/*要插入的元素*/
		int tmp = A[i];
		/*从已有序序列尾向前寻找合适位置*/
		int j = i - 1;
		for (; j >= 0; --j)
		{
			if (A[j] > tmp)
				A[j + 1] = A[j];
			else
				break;
		}//for
		A[j + 1] = tmp;
	}
	return A;
}

二、希尔排序(插入排序)

1、思想希尔排序也是一种插入排序方法,实际上是一种分组插入方法。先取定一个小于n的整数d1作为第一个增量,

把表的全部记录分成d1个组,所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序;

然后,取第二个增量d2(<d1),重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1<…<d2<d1),即所有记录放在

同一组中进行直接插入排序为止。    

     例如:将 n 个记录分成 d 个子序列: 
       { R[0],   R[d],     R[2d],…,     R[kd] } 
       { R[1],   R[1+d], R[1+2d],…,R[1+kd] } 
         … 
       { R[d-1],R[2d-1],R[3d-1],…,R[(k+1)d-1] }

     经典排序算法总结_第1张图片 
     说明:d=5 时,先从A[d]开始向前插入,判断A[d-d],然后A[d+1]与A[(d+1)-d]比较,如此类推,这一回合

后将原序列分为d个组。<由后向前>

2、时间复杂度。  
     最好情况
:由于希尔排序的好坏和步长d的选择有很多关系,因此,目前还没有得出最好的步长如何选择

(现在有些比较好的选择了,但不确定是否是最好的)。所以,不知道最好的情况下的算法时间复杂度。  
     最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。  
     平均情况下:O(N*logN)

3、稳定性。  
     由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序

过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)

4、代码

/*
对于一个int数组,请编写一个希尔排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
*/
#include <iostream>
#include <cstdlib>
using namespace std;
/*希尔排序*/
void ShellSort(int *A, int n)
{
	if (A == NULL || n <= 0)
		return;
	/*设置增量*/
	int d = n;
	while (d > 1)
	{
		d = (d + 1) / 2;
		for (int i = 0; i < n - d; ++i)
		{
			if (A[i + d] < A[i])
			{
				int tmp = A[i + d];
				A[i + d] = A[i];
				A[i] = tmp;
			}//if
		}//for
	}//while
}

三、冒泡排序(交换排序)

1、基本思想:通过无序区中相邻记录关键字间的比较和位置的交换,使关键字最小的记录如气泡一般逐渐往上“漂浮”直至“水面”。 
       
image2、时间复杂度  
     最好情况下:
正序有序,则只需要比较n次。故,为O(n)  
     最坏情况下:  逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(N*N)

3、稳定性  
      排序过程中只交换相邻两个元素的位置。因此,当两个数相等时,是没必要交换两个数的位置的。所以,它们的相对位置并没有改变,冒泡排序算法是稳定的

4、代码

/*
对于一个int数组,请编写一个冒泡排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
*/
#include <iostream>
#include <cstdlib>
using namespace std;
int *BubbleSort(int *A, int n)
{
	if (n <= 0)
		return A;

	/*进行n-1趟冒泡*/
	for (int i = 0; i < n - 1; ++i)
	{
		/*每次冒泡针对头到n-i-1尾比较工作*/
		for (int j = 0; j < n - i - 1; ++j)
		{
			if (A[j] > A[j + 1])
			{
				int tmp = A[j + 1];
				A[j + 1] = A[j];
				A[j] = tmp;
			}//if
		}//for
	}//for
	return A;
}

四、快速排序(交换排序)

1、思想:它是由冒泡排序改进而来的。在待排序的n个记录中任取一个记录(通常取第一个记录),把该记录放入适当位置后,数据序列被此记录划分成两部分。所有关键字比该记录关键字小的记录放置在前一部分,所有比它大的记录放置在后一部分,并把该记录排在这两部分的中间(称为该记录归位),这个过程称作一趟快速排序。

经典排序算法总结_第2张图片           说明:最核心的思想是将小的部分放在左边,大的部分放到右边,实现分割。        
2、算法复杂度  
      最好的情况下
:因为每次都将序列分为两个部分(一般二分都复杂度都和logN相关),故为 O(N*logN) 
      最坏的情况下:基本有序时,退化为冒泡排序,几乎要比较N*N次,故为O(N*N)

3、稳定性  
      由于每次都需要和中轴元素交换,因此原来的顺序就可能被打乱。如序列为 5 3 3 4 3 8 9 10 11会将3的顺序打乱。所以说,快速排序是不稳定的!

4、代码

<span style="font-size:12px;">/*
对于一个int数组,请编写一个归并排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
*/

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <stack>

using namespace std;

/*划分函数*/
int partition(int *A, int left, int right);
/*递归实现*/
void QuickSort(int *A, int left, int right);
/*非递归实现*/
void QuickSort2(int *A, int left, int right);
void swap(int &a, int &b)
{
	int tmp = a;
	a = b;
	b = a;
}
int partition(int *A, int left, int right)
{
	///*随机选择*/
	srand((int)time(NULL));
	int pos = left + (rand() % (right - left + 1));
	///*交换*/
	swap(A[pos], A[right]);

	/*设置左侧小于等于区间{},初始化为left左侧*/
	int lessPos = left - 1;
	int pivot = A[right];
	for (int i = left; i < right; ++i)
	{
		if (A[i] <= pivot)
		{
			/*交换,并小于等于区间尾部右移*/
			++lessPos;
			swap(A[lessPos], A[i]);
		}//if
	}
	swap(A[lessPos + 1], A[right]);
	return lessPos + 1;
}
/*快速排序*/
int *QuickSort(int *A, int n)
{
	if (A == NULL || n <= 0)
		return A;
	QuickSort2(A, 0, n-1);
	return A;
}
/*快速排序的递归实现*/
void QuickSort(int *A, int left, int right)
{
	if (left < right)
	{
		int pos = partition(A, left, right);
		QuickSort(A, left, pos - 1);
		QuickSort(A, pos + 1, right);
	}//if
}

/*快速排序的非递归实现*/
void QuickSort2(int *A, int left, int right)
{
	stack<int> st;
	if (left < right)
	{
		int mid = partition(A, left, right);
		if (left < mid - 1)
		{
			st.push(left);
			st.push(mid - 1);
		}//if

		if (right > mid + 1)
		{
			st.push(mid + 1);
			st.push(right);
		}//if

		/*其实就是用栈保存每一个待排序子串的首尾元素下标,
		下一次while循环时取出这个范围,对这段子序列进行partition操作*/
		while (!st.empty())
		{
			int rhs = st.top();
			st.pop();
			int lhs = st.top();
			st.pop();

			mid = partition(A, lhs, rhs);

			if (lhs < mid - 1)
			{
				st.push(lhs);
				st.push(mid - 1);
			}//if

			if (rhs > mid + 1)
			{
				st.push(mid + 1);
				st.push(rhs);
			}//if
		}//while
	}//if
}
int main()
{
	int arr[] = { 1, 2, 3, 5, 2, 3 };
	QuickSort(arr, 6);

	for (int i = 0; i < 6; ++i)
		cout << arr[i] << "\t";
	cout << endl;
	system("pause");
	return 0;
}</span>


五、直接选择排序(选择排序)

1、思想:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。具体做法是:选择最小的元素与未排序部分的首部交换,使得序列的前面为有序。  
经典排序算法总结_第3张图片2、时间复杂度。 
      最好情况下:
交换0次,但是每次都要找到最小的元素,因此大约必须遍历N*N次,因此为O(N*N)。减少了交换次数! 
      最坏情况下,平均情况下:O(N*N)

3、稳定性 
      由于每次都是选取未排序序列A中的最小元素x与A中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的!

4、代码

#include <iostream>
#include <cstdlib>

using namespace std;
int *SelectionSort(int *A, int n)
{
	if (n <= 0)
		return A;

	/*进行n-1趟选择*/
	for (int i = 0; i < n - 1; ++i)
	{
		/*每次选择,找到i到n-1处最小元素位置,放在i位置*/
		int minPos = i;
		for (int j = i+1; j < n; ++j)
		{
			if (A[j] < A[minPos])
			{
				minPos = j;
			}//if
		}//for

		/*将最小元素放在此趟选择的首位*/
		int tmp = A[i];
		A[i] = A[minPos];
		A[minPos] = tmp;
	}//for
	return 0;
}

六、堆排序

     1、思想:利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或者最小)的记录。也就是说,以最小堆为例,根节点为最小元素,较大的节点偏向于分布在堆底附近。 
经典排序算法总结_第4张图片      2、算法复杂度 
         最坏情况下,接近于最差情况下:O(N*logN),因此它是一种效果不错的排序算法。

      3、稳定性 
         堆排序需要不断地调整堆,因此它是一种不稳定的排序

      4、代码

#include <iostream>
#include <time.h>
#include <cstdlib>
#define N 10
using namespace std;

//声明建大顶堆函数
void BuildMaxHeap(int * array);
//声明堆排序函数
void HeapSort(int * array);
//声明调整为大顶堆函数
void MaxHeapify(int * array, int n);
//返回堆的数据个数
int HeapSize;

int main()
{
	//声明一个待排序数组
	int array[N];
	//设置随机化种子,避免每次产生相同的随机数 
	srand(time(0));
	for (int i = 0; i < N; i++)
	{
		array[i] = rand() % 101;//数组赋值使用随机函数产生1-100之间的随机数   
	}
	cout << "排序前:" << endl;
	for (int j = 0; j < N; j++)
	{
		cout << array[j] << "  ";
	}
	cout << endl << "排序后:" << endl;
	//调用堆排序函数对该数组进行排序   
	HeapSort(array);
	for (int k = 0; k < N; k++)
	{
		cout << array[k] << "  ";
	}
	cout << endl;
	system("pause");
	return 0;
}

void HeapSort(int * array)
{
	BuildMaxHeap(array);
	for (int i = N - 1; i >= 0; i--)//数组中下标从0  -  N-1
	{
		int temp = array[0];
		array[0] = array[i];
		array[i] = temp;
		HeapSize -= 1;
		MaxHeapify(array, 1);//在堆中,堆顶元素下标从1开始
	}
}

void BuildMaxHeap(int * array)
{
	HeapSize = N;
	for (int i = N / 2; i >= 1; i--)//注意i的取值,堆的高度从1  -  N/2
	{
		MaxHeapify(array, i);
	}
}

void MaxHeapify(int * array, int temp)
{
	int largest;//以temp为顶点的子树的堆顶
	int l = 2 * temp;//求以temp为顶点的子树左儿子
	int r = 2 * temp + 1;//求以temp为顶点的子树右儿子

	if (l <= HeapSize && array[l - 1] > array[temp - 1])//首先判断左儿子是否存在,即l<=HeapSize
	{
		largest = l;
	}
	else{
		largest = temp;
	}

	if (r <= HeapSize && array[r - 1] > array[largest - 1])//首先判断右儿子是否存在,即r<=HeapSize
	{
		largest = r;
	}

	if (largest != temp)
	{
		int t = array[temp - 1];
		array[temp - 1] = array[largest - 1];
		array[largest - 1] = t;
		MaxHeapify(array, largest);//调整为大顶堆
	}
}

七、归并排序

      1、思想:多次将两个或两个以上的有序表合并成一个新的有序表。 
经典排序算法总结_第5张图片       2、算法时间复杂度 
          最好的情况下
:一趟归并需要n次,总共需要logN次,因此为O(N*logN) 
          最坏的情况下,接近于平均情况下,为O(N*logN) 
          说明:对长度为n的文件,需进行logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

      3、稳定性 
         归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。 
      4、缺点是,它需要O(n)的额外空间。但是很适合于多链表排序。 
      5、代码

/*
对于一个int数组,请编写一个归并排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
*/

#include <iostream>
#include <cstdlib>

using namespace std;
void MergeSort(int *A, int left, int right);
void Merge(int *A, int left, int mid, int right);

int* MergeSort(int *A, int n)
{
	if (A == NULL || n <= 0)
		return A;

	MergeSort(A, 0, n - 1);
	return A;
}

void MergeSort(int *A, int left, int right)
{
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	MergeSort(A, left, mid);
	MergeSort(A, mid + 1, right);

	Merge(A, left, mid, right);
}

/*将排序后的left~mid与mid+1~right两个子序列合并*/
void Merge(int *A, int left, int mid, int right)
{
	int *tmp = new int[right - left + 1];
	/*合并排序后的元素到tmp临时数组*/
	int lhs = left, rhs = mid + 1, k = 0;
	while (lhs <= mid && rhs <= right)
	{
		if (A[lhs] <= A[rhs])
			tmp[k++] = A[lhs++];
		else
			tmp[k++] = A[rhs++];
	}//while

	/*直接拷贝剩余元素*/
	while (lhs <= mid)
	{
		tmp[k++] = A[lhs++];
	}//while

	while (rhs <= right)
	{
		tmp[k++] = A[rhs++];
	}//while

	/*拷贝临时数组数据到原数组*/
	for (int i = left; i <= right; ++i)
	{
		A[i] = tmp[i - left];
	}//for

	delete[]tmp;
}


//int main()
//{
//	int arr[] = { 1, 2, 3, 5, 2, 3 };
//	MergeSort(arr, 6);
//
//	for (int i = 0; i < 6; ++i)
//		cout << arr[i] << "\t";
//	cout << endl;
//	system("pause");
//	return 0;
//}

八、基数排序

      1、思想:它是一种非比较排序。它是根据位的高低进行排序的,也就是先按个位排序,然后依据十位排序……以此类推。示例如下: 
经典排序算法总结_第6张图片
经典排序算法总结_第7张图片        2、算法的时间复杂度 
       分配需要O(n),收集为O(r),其中r为分配后链表的个数,以r=10为例,则有0~9这样10个链表来将原来的序列分类。而d,也就是位数(如最大的数是1234,位数是4,则d=4),即"分配-收集"的趟数。因此时间复杂度为O(d*(n+r))。

       3、稳定性 
          基数排序过程中不改变元素的相对位置,因此是稳定的!

       4、适用情况:如果有一个序列,知道数的范围(比如1~1000),用快速排序或者堆排序,需要O(N*logN),但是如果采用基数排序,则可以达到O(4*(n+10))=O(n)的时间复杂度。算是这种情况下排序最快的!!



注:原文解析部分转载自原文链接



  


你可能感兴趣的:(经典排序算法总结)