对7种排序算法的总结

排序算法是最基本的知识了,面试中也是常考的知识点,尤其是快速排序考察最多,也要求手写出来,并分析他的时间复杂度。下面是我对所学到的知识的一个总结。

排序算法总共分为5大类:
第一类:插入排序 直接插入、希尔排序
第二类:选择排序简单选择排序 堆排序
第三类:交换排序冒泡排序 快速排序
第四类:归并排序
第五类:基数排序(考察很少,知道概念就行)

在这里插入图片描述

直接插入排序

方法:假设元素从小到大排序,先固定第一个元素,让第二个元素开始往后查找,把后面的元素放到一个临时变量里面拎出来,让他和他前面的元素比较,如果比前面的元素小,就把被比较的元素向后面移动一格,并且指针向前移动一位,确保临时变量是依次和前面的元素比较的。此方法需要两个指针。填充位置为每次比较排好序的元素。
需要注意确定边界值,j指针>=0
时间复杂度计算:(1+n)*n/2=O(n^2)相当于九九乘法表的图形
在这里插入图片描述

#include <iostream>
using namespace std;

void StraightInsertion(int a[], int length)
{
	int i = 1, j = 0;
	int temp = 0;
	/*当前排到第几个元素*/
	for (i;i<length;i++)
	{
		/*保存当前的元素*/
		temp = a[i];
		/*依次检索是否小于前面的元素*/
		j = i - 1;
		while (temp < a[j] && j >= 0)
		{
			/*当前被检测元素向后移动一格*/
			a[j + 1] = a[j];
			j--;
		}
		
		/*找到插入位置*/
		
			a[j + 1] = temp;
	}
}


int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	StraightInsertion(a, 10);

	return 0;
}

希尔排序

方法:把整个一串数按照/2进行分组,10个数/2先分成5组,然后5/2=2,再分成两组,直到为1,这样再进行组内直接插入排序。只是单纯的比直接插入排序多了一个分组的步骤。
代码和直接插入几乎一样,只不过把j移动的距离从1改为组的个数h
平均时间复杂度提高了,可以做到部分有序,再进行直接插入
这里注意:组间是交叉进行组内比较的,不是一个组进行完进行下一个组。
该算法是不稳定的。

在这里插入图片描述

#include <iostream>
using namespace std;

void shellSort(int a[], int length)
{
	int i = 0, j = 0, temp = 0, h = length/2;/*组的个数*/
	//分组
	for (h;h>=1;h/=2)
	{
		/*各组交替比较组内的元素 也就是使用直接插入排序*/
		for (i = h; i < length; i++)
		{
			temp = a[i];
			j = i - h;
			while (temp < a[j] && j >= 0)
			{
				/*当前被检测元素在组内向后移动一个元素*/
				a[j + h] = a[j];
				j -= h;
			}
			/*找到插入位置*/
			a[j + h] = temp;
		}
	}
}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	
	shellSort(a, 10);
	return 0;
}

简单选择排序

思想:从一堆无序的数字中,先选择一个最小的,然后选择次小的,一次类推。。是最简单的排序,符合人们的正常思维。
方法:需要额外开辟一个数组,用来存储拿出来的元素。首先让一串数字的首元素默认是当前最小值,记录他的标号,用min_index表示,然后让后面的元素依次和标号所只的元素比较,若比他小,记录下标号,先不着急交换,因为没有比较完,后面可能有更小的,直到比较到头,然后此时
min_index是最小的值的标号,然后将第一个元素和其交换,此时最小的找到,接着找次小的,此时i指针后移一位,j接着从i的下一个开始往后捋。(i的范围是倒数第二个数字,不用再比较最后欧一个元素,因为前面都是小的了,自然最后的是最大的值)。
该算法不稳定,对未排序的算法进行了未知的修改。
在这里插入图片描述

#include <iostream>
using namespace std;

void exchange(int a[], int i, int j)
{
	int temp = 0;
	temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}
void selectionSort(int a[], int n)
{
	int i, j, min_index;
	for ( i = 0; i < n - 1; i++)
	{
		min_index = i;
		for (j=i+1;j<n;j++)
		{
			if (a[j] < a[min_index])
				/*找到本次最小值*/
				min_index = j;
		}
		if (min_index != i)
			/*交换*/
			exchange(a, min_index, i);

	}

}



int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	selectionSort(a, 10);
	
	return 0;
}

堆排序

需要用到树的结构,需要的是一个完全二叉树。
步骤:
1。建立一个大根/小根堆
2.堆化
3.直接选择排序
若从小到大排序,先建立一个大根堆,然后按层遍历到一个数组,然后将收尾元素交换,此时保证了最后的元素是最大的值,之后让iCurront指针向前移动一位,将前面的元素进行堆化,用到递归。递归的终止条件是该元素是叶子结点,没有左孩子了,也就是说该元素的左孩子已经是排好序的无效结点了。如何找到那个有效结点呢?公式:(n-1)/2 n代表最后的元素下标
该算法不稳定。

在这里插入图片描述

#include <iostream>
using namespace std;

int iCurrentPos = 0;

void exchange(int a[], int b, int c)
{
	int temp = 0;
	temp = a[b];
	a[b] = a[c];
	a[c] = temp;
}
void heapify(int a[], int indexRoot)
{
	int indexL = indexRoot * 2 + 1, indexR = indexL + 1;
	int indexMax = indexRoot;
	/*终止条件,当时叶子节点的时候 如何判断是否是叶子节点,
	就是看他的左孩子是否是无效的节点
	*/
	if (indexL > iCurrentPos)
		return;
	/*比较左子节点*/
	if (a[indexL] > a[indexMax])
		indexMax = indexL;
	/*比较右子节点*/
	if (a[indexR] > a[indexMax] && indexR < iCurrentPos)
		indexMax = indexR;
	/*交换结点*/
	if (indexMax != indexRoot)
	{
		exchange(a, indexRoot, indexMax);
		heapify(a, indexMax);
	}
	
}

/*建立一个大根堆*/
void createHeap(int a[], int n) //n为第n个元素
{
	/*找到有效的结点*/
	int i = (n - 1) / 2;
	for (i; i >= 0; --i)
	{
		heapify(a, i);

	}
}

/*堆排序*/

void heapSort(int a[], int n) /*n指的总共的元素个数*/
{
	iCurrentPos = n - 1;
	createHeap(a, n - 1);
	for (; iCurrentPos > 0; --iCurrentPos, heapify(a, 0))
		exchange(a, 0, iCurrentPos);

}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	heapSort(a, 10);
	
	return 0;
}

冒泡排序

冒泡排序就是两两交换,然后每一次遍历都会让最大放在数组的最后。然后接着从头俩俩比较。
在这里插入图片描述

#include <iostream>
using namespace std;

void exchange(int a[], int i, int j)
{
	int temp = 0;
	temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}
void SimpleExchange(int a[], int length)
{
	int i = 0,/*已排序数量*/ j = 1;
	for (i = 0; i < length - 1; i++)
	{
		for (j = 1; j < length - i; j++)
		{
			/*检查是否交换*/
			if (a[j] < a[j - 1])
				exchange(a,j - 1, j);
		}
	}
}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	SimpleExchange(a, 10);

	return 0;
}

快速排序

典型的递归的算法。目的是找到一个轴pivot,让左边的都比他小,右边的都比他大。
如何找到这个值的位置呢?
让头尾各指一个指针,把首元素的值拿出来,让他作为轴,位置空缺,让尾元素和他比较,是大的就留在位置不动,然后移动j指针向前,直到找到一个元素的值比第一个小,就让其放在空缺的位置,此时他的位置空缺,再让i指针向后移动,和拿出的那个首元素接着比较,若小,就不动,指针后移,直到找到比他大的元素,放在空白位置。然后尾指针往前移动。。。直到 i j指针重合就是首元素该插入的位置,就是中间元素,保证了左侧的都是小的,右侧都是大的.然后左右递归。
快排的时间复杂度分析
在这里插入图片描述
最佳时间复杂度是nlogn 每次分割成两半,分多少层呢?就是N不断的/2,也就是logn层,每层的从左到右交换都是n的操作,所以时间复杂度最佳是nlogn
最坏时间复杂度n^2 在最坏的情况下,待排序的序列为正序或者逆序,每次划分只得到一个比上一次划分少一个记录的子序列。也就是说每次都是1个数和其余要进行递归的序列。这样就需要n层,每层是n的操作 即复杂度是n^2
平均复杂度就是第一次划分是最佳的划分,但是左右侧就是有最坏的情况,平均下来还是nlogn
空间复杂度就是每分一层,都会有一些临时变量存在,这样就是看有多少层,就是nlongn
快排的性能和什么有关系?
和pivot选取的方法有关系。pivot是可以随便选择的,但是要注意选到的值是中间的值,还是过大或者过小的。要是极值可能就会得到最坏的时间复杂度。
那如何避免最坏时间复杂度呢如何优化快排呢
1.常用的方法就是三数取中。就是找到头,中间和尾的元素,哪个是中间值,就把哪个作为轴。
2.直接插入
由于是递归程序,每一次递归都要开辟栈帧,当递归到序列里的值不是很多时,我们可以采用直接插入排序来完成,从而避免这些栈帧的消耗。
还有一种方法,用的不是很多
让第一个元素的左边放一个可以扩展的区域,假设最后的元素是轴,从第一个元素开始比较,如果比轴小,就把他包括进这个区域,如果大了,就把轴前面的元素和这个元素做交换,此时检查交换后的这个元素是不是还是比轴大,若还是就把他接着和轴前一个的前一个交换。。直到和后面交换的元素重合,然后轴剩下了,放在什么位置呢?就把他和区的下一个位置做交换。
具体还有好几种方法,就不一一列举了。这篇博客说的很明白。
详见可见博客地址

如今的快排和直接插入联合使用了。快排当到一定的深度,量少,个数少趋于有序的时候就不占有优势,适合直接插入,所以将两者联合使用效果更佳。微软目前使用的就是这样的排序算法。

#include <iostream>
using namespace std;

void QuickSort(int a[], int min,int max)
{
	/*递归的结束条件*/
	if (min >= max)
		return;
	int pivot = a[min]; 
	int _min = min, _max = max;
	while (min < max)
	{
		/*从大端开始比较*/
		while (a[max] >= pivot && min<max)
			max--;
		/*如果不再大于等于pivot 就进行交换*/
		if (min < max)
		{
			a[min] = a[max];
			min++;
		}
		/*从小端开始比较*/
		while (a[min] <= pivot && min < max)
			min++;
		/*如果不再小于等于pivot,就交换*/
		if (min < max)
		{
			a[max] = a[min];
			max--;
		}

			
	}

	/*找到中枢的位置pivot*/
	a[min] = pivot;
	/*递归左侧*/
	QuickSort(a, _min, min - 1);
	/*递归右侧*/
	QuickSort(a, max + 1, _max);

}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	QuickSort(a,0,9);
	
	return 0;
}

归并排序

视频链接
归并算法和前几种都不一样,是一个特殊的算法,他就是两个步骤,先分解,再整合。
Divide and Conquer(分而治之)

实现思路:我们需要开辟一个数组,然后将分解出的数组在头尾均设立指针,左侧最小,左侧最大,右侧最小,右侧最大。数组需要遍历的元素就是左侧最小到右侧最大。
归并时分三种情况讨论:
1.当左右两侧均没有到末尾,就比较左右的谁最小,放到数组的相应位置。然后移动指针和数组的指针。
2.当左侧到了头,就说明右侧有小的,就把右侧的元素给数组并移动指针。
3.当右侧到了头,说明左侧有小的。把左侧的元素给数组并移动指针。
在这里插入图片描述

#include <iostream>
using namespace std;


void merge(int a[], int min_l, int max_l, int min_r, int max_r,int temp[])
{
	int k, i = min_l, j = min_r;
	for (k=min_l;k<=max_r;k++)
	{
		/*左右两侧均没到结尾*/
		if (i <= max_l && j <= max_r)
		{
			if (a[i] <= a[j])
				temp[k] = a[i++];
			else
				temp[k] = a[j++];
		}
		/*左侧到了结尾*/
		else if (i>max_l)
			temp[k] = a[j++];
		/*右侧到结尾*/
		else
			temp[k] = a[i++];

	}
	/*把值付给数组a 数据填回*/
	for (k=min_l;k<=max_r;k++)
	{
		a[k] = temp[k];

	}
}
void MergingSort(int a[],int min,int max,int temp[])
{
	 /*终止条件*/
	if (min>=max)
		return;

	/*divide*/
	int middle = min+(max - min) / 2;
	MergingSort(a, min, middle,temp);
	MergingSort(a, middle + 1, max,temp);
	/*conquer*/
	merge(a, min, middle, middle + 1, max,temp);
}


int main(void)
{
	int b[10] = {0};
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	MergingSort(a, 0, 9, b);
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章