常见经典排序算法学习总结(插入、shell、冒泡、选择、归并、快排等)

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

      博主在学习过程中深感基础的重要,经典排序算法是数据结构与算法学习过程中重要的一环,这里对笔试面试最常涉及到的7种排序算法(包括插入排序、希尔排序、选择排序、冒泡排序、快速排序、堆排序、归并排序)进行了详解。每一种算法都有基本介绍、算法原理分析、算法代码。

插入排序

1)算法简介

插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

2)算法描述和分析

具体算法描述如下:

1. 从第一个元素开始,该元素可以认为已经被排序

2. 取出下一个元素,在已经排序的元素序列中从后向前扫描

3. 如果该元素(已排序)大于新元素,将该元素移到下一位置

4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

5. 将新元素插入到该位置后

6. 重复步骤2~5

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

3)算法代码

void InsertSort(int a[], int n)
{
	//循环变量
	int i,j;
	//中间变量
	int temp;
	for (i=1; i<n; i++)
	{
		temp=a[i];
		//从后向前循环,将a[0]~a[i-1]中大于temp的值后移
		for (j=i-1; j>=0&&a[j]>temp; j--)
			a[j+1]=a[j];
		//将temp放入合适位置
		a[j+1]=temp;
	}
}

希尔排序

1)算法简介

希尔排序,也称缩小增量排序算法,名称源于它的发明者Donald Shell,是插入排序的一种高速而稳定的改进版本。

2)算法描述

1、先取一个增量把元素分割成若干个子序列,对各子序列分别进行直接插入排序。

2、依次缩减增量再进行排序。

3、直至所取的增量足够小时,再进行一次直接插入排序。

希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(3/2)),但是现今仍然没有人能找出希尔排序的精确下界。

3)算法代码

void ShellSort(int a[], int n)
{
	//循环变量
	int i,j;
	//增量
	int increment;
	//中间变量
	int temp;
	for (increment=n/2; increment>0; increment/=2)
	{
		//在增量分割的子序列中进行插入排序
		for (i=increment; i<n; i++)
		{
			temp=a[i];
			for (j=i-increment; j>=0&&a[j]>temp; j-=increment)
			{
				//右移
				a[j+increment]=a[j];
			}
			a[j+increment]=temp;
		}
	}
}

冒泡排序

1)算法简介

     冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

2)算法描述

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

2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

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

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

冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。

最差时间复杂度     O(n^2)

最优时间复杂度     O(n)

平均时间复杂度     O(n^2)

最差空间复杂度     总共O(n),需要辅助空间O(1)

3)算法代码

void BubbleSort(int a[], int n)
{
	int i,j;
	//中间变量
	int temp;
	for (i=0; i<n; i++)
	{
		for (j=0; j<n-1-i; j++)
		{
			//交换
			if (a[j+1]<a[j])
			{
				temp=a[j];
				a[j]=a[j+1];
				a[j+1]=temp;
			}
		}
	}
}

选择排序

1)算法简介

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法

2)算法描述和分析

1、初始状态:无序区为R[1..n],有序区为空,令i=0

2、在无序区R[i..n-1]中选出关键字最小的记录 R[k],将它与无序区的第1个记录R[i]交换,交换之后R[0…i]就形成了一个有序区。

3i++并重复第二步,直到i==n-1,数组有序化了。

3)算法代码

void SelectSort(int a[], int n)
{
	//循环变量
	int i,j;
	//最小元素的下标
	int mindex;
	//中间变量
	int temp;
	for (i=0; i<n; i++)
	{
		mindex=i;
		for (j=i+1; j<n; j++)
		{
			if (a[j]<a[mindex])
			{
				mindex=j;
			}
		}
		temp=a[i];
		a[i]=a[mindex];
		a[mindex]=temp;
	}
}

归并排序

1)算法简介

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

       将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

2)算法描述和分析

归并排序具体算法描述如下(递归版本):

1、Divide: 把长度为n的输入序列分成两个长度为n/2的子序列。

2、Conquer: 对这两个子序列分别采用归并排序。

3、Combine: 将两个排序好的子序列合并成一个最终的排序序列。

归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。

3)算法代码

//将两个有序数列a[first~mid]和a[mid+1~last]合并
void merge(int a[], int pTemp[], int first, int mid, int last)
{
	int i=first,j=mid+1,k=first;
	while(i<=mid&&j<=last)
	{
		if (a[i]<a[j])
		{
			pTemp[k++]=a[i++];
		}
		else
			pTemp[k++]=a[j++];
	}
	while(i<=mid)
		pTemp[k++]=a[i++];
	while(j<=last)
		pTemp[k++]=a[j++];
	for (i=first; i<=last; i++)
		a[i]=pTemp[i];
}

//归并排序
void MSort(int a[], int pTemp[], int left, int right)
{
	int Center;
	if (left<right)
	{
		Center=(left+right)/2;
		MSort(a,pTemp,left,Center);//左边有序
		MSort(a,pTemp,Center+1,right);//右边有序
		merge(a,pTemp,left,Center, right);//将两个有序数列合并
	}
}

bool MergeSort(int a[], int n)
{
	int *pTempArray;
	pTempArray=(int *)malloc(n*sizeof(int));
	if (pTempArray==NULL)
		return false;
	MSort(a,pTempArray,0,n-1);
	free(pTempArray);
	return true;
}

堆排序

1)算法简介

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

   堆排序方法对记录数较少的文件并不值得提倡,但对n较大的文件还是很有效的。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。

  堆排序在最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需一个记录大小的供交换用的辅助存储空间。

2)堆的定义

n个元素的序列{k1k2…,kn}当且仅当满足下列关系之一时,称之为堆。

  情形1ki <= k2i ki <= k2i+1 (最小化堆或小顶堆)

  情形2ki >= k2i ki >= k2i+1 (最大化堆或大顶堆)

其中i=1,2,…,n/2向下取整;

若将和此序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。

  由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。

若在输出堆顶的最小值之后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值。如此反复执行,便能得到一个有序序列,这个过程称之为堆排序

3)堆的存储

  一般用数组来表示堆,若根结点存在序号0处, i结点的父结点下标就为(i-1)/2。i结点的左右子结点下标分别为2*i+1和2*i+2。(注:如果根结点是从1开始,则左右孩子结点分别是2i和2i+1。)

4)堆排序的实现

实现堆排序需要解决两个问题:

  1.如何由一个无序序列建成一个堆?

2.如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

  先考虑第二个问题,一般在输出堆顶元素之后,视为将这个元素排除,然后用表中最后一个元素填补它的位置,自上向下进行调整:首先将堆顶元素和它的左右子树的根结点进行比较,把最小的元素交换到堆顶;然后顺着被破坏的路径一路调整下去,直至叶子结点,就得到新的堆。

  我们称这个自堆顶至叶子的调整过程为筛选。从无序序列建立堆的过程就是一个反复“筛选”的过程。

5)构造初始堆

  初始化堆的时候是对所有的非叶子结点进行筛选。最后一个非终端元素的下标是[n/2]向下取整,所以筛选只需要从第[n/2]向下取整个元素开始,从后往前进行调整。

  比如,给定一个数组,首先根据该数组元素构造一个完全二叉树。然后从最后一个非叶子结点开始,每次都是从父结点、左孩子、右孩子中进行比较交换,交换可能会引起孩子结点不满足堆的性质,所以每次交换之后需要重新对被交换的孩子结点进行调整。

6)进行堆排序

  堆排序是一种选择排序。建立的初始堆为初始的无序区。

  排序开始,首先输出堆顶元素(因为它是最值),将堆顶元素和最后一个元素交换,这样,第n个位置(即最后一个位置)作为有序区,前n-1个位置仍是无序区,对无序区进行调整,得到堆之后,再交换堆顶和最后一个元素,这样有序区长度变为2。

不断进行此操作,将剩下的元素重新调整为堆,然后输出堆顶元素到有序区。每次交换都导致无序区-1,有序区+1。不断重复此过程直到有序区长度增长为n-1,排序完成。

 由排序过程可见,若想得到升序,则建立大顶堆,若想得到降序,则建立小顶堆

7)算法代码

// 输入数组A,堆的长度len,以及需要调整的节点i,调堆
void HeapAdjust(int A[], int len, int i)
{
    int left=2*i+1;//结点i的左孩子
	int right=2*i+2;//结点i的右孩子
	int largest=i;
	int temp;
	while(left<len||right<len)
	{
		if (left<len&&A[left]>A[largest])
		{
			largest=left;
		}
		if (right<len&&A[right]>A[largest])
		{
			largest=right;
		}
		//如果最大值不是父结点
		if (i!=largest)
		{
			//交换父结点和拥有最大值的子结点
			temp=A[i];
			A[i]=A[largest];
			A[largest]=temp;
			//新的父结点,以备迭代调堆
			i=largest;
			//新的子结点
			left=2*i+1;
			right=2*i+2;
		}
		else
			break;
	}
}

//建堆
void BuildHeap(int A[], int len)
{
	//最后一个非叶子结点
	int begin=len/2-1;
	for (int i=begin; i>=0; i--)
	{
		HeapAdjust(A,len,i);
	}
}

//堆排序
void HeapSort(int A[], int n)
{
	int temp;
	//建堆
	BuildHeap(A,n);
	while(n>1)
	{
		//交换堆的第一个元素和最后一个元素
		temp=A[n-1];
		A[n-1]=A[0];
		A[0]=temp;
		n--;
		//调堆
		HeapAdjust(A,n,0);
	}
}

快速排序

快速排序是各种笔试面试最爱考的排序算法之一,且排序思想在很多算法题里面被广泛使用。是需要重点掌握的排序算法。

1)算法简介

快速排序是由东尼·霍尔所发展的一种排序算法。其基本思想是,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

2)算法描述和分析

快速排序使用分治法来把一个串(list)分为两个子串行(sub-lists)。

步骤为:

1、从数列中挑出一个元素,称为 "基准"(pivot),

2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

平均状况下,排序n个项目要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

最差时间复杂度    O(n^2)

最优时间复杂度    O(n log n)

平均时间复杂度    O(n log n)

最差空间复杂度   根据实现的方式不同而不同

我们选取数组的第一个元素作为主元,每一轮都是和第一个元素比较大小,通过交换,分成大于和小于它的前后两部分,再递归处理。

3)算法代码

void QuickSort(int a[], int left, int right)
{
	int i,j,v;
	if (left<right)
	{
		i=left;
		j=right;
		//以本次最左边的值为标准进行划分
		v=a[i];
		do 
		{
			//从右向左找第一个小于标准位置j
			while(a[j]>v&&i<j)
				j--;
			if (i<j)
			{
				a[i]=a[j];
				i++;//将第j个元素置于左端,并重置i
			}
			//从左向右找第一个大于标准位置i
			while(a[i]<v&&i<j)
				i++;
			if (i<j)
			{
				a[j]=a[i];
				j--;//将第i个元素置于右端,并重置j
			}
		} while (i!=j);
		//将标准值放入它的最终位置
		a[i]=v;
		//对标准值左半部分递归
		QuickSort(a,left,i-1);
		//对标准值右半部分递归
		QuickSort(a,i+1,right);
	}
}

总结

总结一下各种排序算法如下:

名称

时间复杂度

额外空间

稳定性

    考点

插入排序

平均O(n^2)

最优O(n)

最差O(n^2)

O(1)

 稳定

选择填空

各种时间复杂度

移动元素个数

希尔排序

最差O(n log n)

最优 O(n)

O(n)

 不稳定

时间复杂度

比较次数

选择排序

O(n^2)

O(1)

 不稳定

同插入排序

冒泡排序

O(n^2)

最优O(n)

最差O(n^2)

O(1)

 稳定

时间复杂度

比较次数

单轮冒泡

快速排序

O(n log n)

O(1)

 不稳定

时间复杂度

快排partition算法

堆排序

O(n log n)

O(n)

 

 不稳定

时间复杂度

堆调整,建堆,堆排序,Top K问题

归并排序

平均O(nlogn)

最差O(nlogn)

最优O(n)

O(n)

 

******************

作者:hao_09

时间:2015/8/4

文章地址:http://blog.csdn.net/lsh_2013/article/details/47280135

******************












猜你喜欢

转载自blog.csdn.net/u010418035/article/details/47280135