常见经典排序算法学习总结,附算法原理及实现代码(插入、shell、冒泡、选择、归并、快排等)

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

转载请注明出处:http://blog.csdn.net/lsh_2013/article/details/47280135

插入排序

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]就形成了一个有序区。

3、i++并重复第二步,直到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个元素的序列{k1,k2,…,kn}当且仅当满足下列关系之一时,称之为堆。

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

  情形2:ki >= 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)

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-22 16:12:31

常见经典排序算法学习总结,附算法原理及实现代码(插入、shell、冒泡、选择、归并、快排等)的相关文章

算法学习笔记 KMP算法之 next 数组详解

最近回顾了下字符串匹配 KMP 算法,相对于朴素匹配算法,KMP算法核心改进就在于:待匹配串指针 i 不发生回溯,模式串指针 j 跳转到 next[j],即变为了 j = next[j]. 由此时间复杂度由朴素匹配的 O(m*n) 降到了 O(m+n), 其中模式串长度 m, 待匹配文本串长 n. 其中,比较难理解的地方就是 next 数组的求法.next 数组的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀,也可看作有限状态自动机的状态,而且从自动机的角度反而更容易推导一些. "前

七种常见经典排序算法总结(C++)

最近想复习下C++,很久没怎么用了,毕业时的一些经典排序算法也忘差不多了,所以刚好一起再学习一遍. 除了冒泡.插入.选择这几个复杂度O(n^2)的基本排序算法,希尔.归并.快速.堆排序,多多少少还有些晦涩难懂,幸好又博客园大神dreamcatcher-cx都总结成了图解,一步步很详细,十分感谢. 而且就时间复杂度来说,这几种算法到底有什么区别呢,刚好做了下测试. 代码参考: http://yansu.org/2015/09/07/sort-algorithms.html //: basic_so

常见经典排序算法

插入排序: 算法简介:接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止.时间复杂度为O(n^2). 最稳定的排序算法但是效率很低 代码实现: void InsertSort(int *arr,int n) {                  for (int index = 0; index < n-1; ++index)                 {             

经典算法学习之贪心算法

贪心算法也是用来求解最优化问题的,相比较动态规划很多问题使用贪心算法更为简单和高效,但是并不是所有的最优化问题都可以使用贪心算法来解决. 贪心算法就是在每个决策点都做出在当时看来最佳的选择. 贪心算法的设计步骤: 1.将最优化问题转换为:对其做出一次选择之后,只剩下一个问题需要求解的形式(动态规划会留下多个问题需要求解) 2.证明做出贪心选择之后,原问题总是存在最优解,即贪心算法总是安全的 3.证明做出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解,这样就得到了最

九大排序算法及其实现- 插入.冒泡.选择.归并.快速.堆排序.计数.基数.桶排序

  闲着的时候看到一篇“九大排序算法在总结”,瞬间觉得之前数据结构其实都有学过,但当初大多数都只是老师随口带过,并没有仔细研究一下.遂觉:这是欠下的账,现在该还了.   排序按照空间分类: In-place sort不占用额外内存或占用常数的内存 插入排序.选择排序.冒泡排序.堆排序.快速排序. Out-place sort:归并排序.计数排序.基数排序.桶排序. 或者按照稳定性分类: stable sort:插入排序.冒泡排序.归并排序.计数排序.基数排序.桶排序. unstable sort

[算法学习]A星算法

一.适用场景 在一张地图中,绘制从起点移动到终点的最优路径,地图中会有障碍物,必须绕开障碍物. 二.算法思路 1. 回溯法得到路径 (如果有路径)采用"结点与结点的父节点"的关系从最终结点回溯到起点,得到路径. 2. 路径代价的估算:F = G+H A星算法的代价计算使用了被称作是启发式的代价函数. 先说明一下各符号意义:G表示的是 ** 从起点到当前结点的实际路径代价 ** (为啥叫实际?就是已经走过了,边走边将代价计算好了):H表示 ** 当前结点到达最终结点的估计代价 ** (为

算法学习(一) -- 基本算法

## 1.插入排序 插入排序法的基本思路:同样以案例来说明,还是以$arr = array(2,6,3,9),由大到小排序. 实现原理:插入排序的思想有点像打扑克抓牌的时候,我们插入扑克牌的做法.想象一下,抓牌时,我们都是把抓到的牌按顺序放在手中.因此每抓一张新牌,我们都将其插入到已有的排好序的手牌当中,注意体会刚才的那句话.也就是说,插入排序的思想是,将新来的元素按顺序放入一个已有的有序序列当中. 代码规律分析:array(23,34,12,56,43,98,89)注意下面括号中元素插入的位置

算法学习之查找算法:动态查找表(1)二叉排序树

引言: 动态查找表的特点是,在表结构本身是在查找过程中动态生成的,即对于给定值key,若表中存在其关键字等于key的记录,则查找成功返回,否则插入关键字等于key的记录. 二叉排序树或者是一棵空树,或者是具有下列性质的二叉树: 1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值. 2.若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值. 3.它的左.右子树也分别为二叉排序树. <一>二叉排序树的查找: 二叉排序树又称二叉查找树,查找过程是先将给定值和根结点的关键字比较,

算法学习之查找算法:静态查找表(1)顺序表查找

引言: 对查找表一般的操作有:1.查询某个"特定的"数据元素是否在查找表中:2.检索某个"特定的"数据元素的各种属性:3.在查找表中插入一个数据元素:4.从查找表中删去某个数据元素. 静态查找表的操作只包括两种:1.查找某个"特定的"数据元素是否在查找表中:2.检索某个"特定的"数据元素的各种属性: 静态查找表又有四种表现形式:顺序表的查找.有序表的查找.静态树的查找.索引顺序表的查找. 静态查找涉及的关键字类型和数据元素类型