zoukankan      html  css  js  c++  java
  • C++ 数据结构 4:排序

    1 基本概念

    1.1 定义

    排序是计算机内经常进行的一种操作,其目的是将一组“无序”的数据元素调整为“有序”的数据元素。

    1.2 数学定义

    假设含n个数据元素的序列为 { R1, R2, …, Rn},其相应的关键字序列为 { K1, K2, …, Kn} 这些关键字相互之间可以进行比较,即在它们之间存在着这样一个关系 : Kp1≤Kp2≤…≤Kpn

    按此固有关系将上式记录序列重新排列为 { Rp1, Rp2, …,Rpn} 的操作称作 排序

    1.3 排序的稳定性

    如果在序列中有两个数据元素 r[i]r[j],它们的关键字 k[i] == k [j],且在排序之前,对象 r[i] 排在 r[j] 前面。如果在排序之后,对象 r[i] 仍在 r[j] 前面,则称这个排序方法是稳定的,否则称这个排序方法是不稳定的。

    1.4 排序中的关键操作

    • 比较:任意两个数据元素通过比较操作确定先后次序

    • 交换:数据元素之间需要交换才能得到预期结果

    1.5 内排序和外排序

    • 内排序:整个排序过程不需要访问外存便能完成

    • 外排序:待排序的数据元素数量很大,整个序列的排序过程不可能在内存中完成

    1.6 总结

    • 排序是数据元素从无序到有序的过程

    • 排序具有稳定性,是选择排序算法的因素之一

    • 比较和交换是排序的基本操作

    • 多关键字排序与单关键字排序无本质区别

    • 排序的时间性能是区分排序算法好坏的主要因素

    2 选择排序

    2.1 算法介绍

    每一次从待排序的数据元素中选出最小(或最大)的一个元素存放在序列的起始位置,直到全部待排序的数据元素拍完。

    2.2 基本思想

    设数组为 a[0] ~ a[n-1]

    1. 初始时,数组全为无序区为 a[0] ~ a[n-1],令 i = 0

    2. 在无序区 a[i] ~ a[n-1] 中选取一个最小的元素,将其与 a[i] 交换,交换后 a[0] ~ a[i] 就形成了一个有序区

    3. i++ 并 重复第 2 步,直到 i == n-1,排序完成。

    简单来说:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

    2.3 算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    //选择排序(升序排列)
    void selectionSort(int *array, int len)
    {
    	int min = 0;	// 指向最小的元素的位置
    
    	int i = -1;
    	int j = -1;
    	int k = -1;
    
    	// 外层循环
    	for (i = 0; i < len - 1; ++i)
    	{
    		min = i;
    
    		// 内存循环
    		for (j = i + 1; j < len; ++j)
    		{
    			// 判断
    			if (array[min] > array[j])
    			{
    				// 保存最小的元素的位置
    				min = j;
    			}
    		}
    
    		// 判断是否需要交换
    		if (min != i)
    		{
    			// 找到了新的最小值
    
    			// 交换
    			int tmp = array[min];
    
    			array[min] = array[i];
    
    			array[i] = tmp;
    		}
    
    
    		printf("第 i =  %d 次,排序后的数组为:
    ",i);
    
    		for (k = 0; k < len; ++k)
    		{
    			printf("%d	", array[k]);
    		}
    
    		printf("
    ");
    	}
    }
    
    #if 1
    void main()
    {
    	int i;
    
    	//定义整型数组
    	int array[] = { 12, 5, 33, 6, 10 };
    	//计算数组长度
    	int len = sizeof(array) / sizeof(int);
    
    	//遍历数组
    	printf("待排序数组序列: ");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	//排序
    	selectionSort(array, len);
    
    	//遍历
    	printf("选择排序之后的序列:	");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	system("pause");
    }
    #endif
    

    运行结果:

    2.4 稳定性

    • 选择排序是不稳定的排序方法

    • 选择排序效率:(O(n^2))

    3 冒泡排序

    3.1 算法介绍

    冒泡排序算法的运作如下:

    从后往前

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

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

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

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

    3.2 基本思想

    设数组长度为 N

    从后往前

    1. 比较相邻的前后两个数据,如果 前面数据大于后面的数据,就将两个数据交换

    2. 这样对数组的第 0 个数据到 N-1 个数据进行一次遍历后,最大的一个数据就“升”到数组第 N-1 个位置

    3. N = N-1,如果 N 不为 0 就重复前面两步,否则排序完成。

    3.3 排序过程

    3.4 算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    //冒泡排序(升序)
    void bubbleSort(int *array, int len)	
    {
    	int i = -1;
    	int j = -1;
    	int k = -1;
    
    	// 判断数组是否已排序好 0: 没有排好, 1: 已经排好
    	int flag = 0;
    
    	int tmp = -1;
    
    	for (i = len - 1; i > 0 && flag == 0; --i)
    	{
    		flag = 1;	// 假设已经排序好
    
    		for (j = 0; j < i; ++j)
    		{
    			if (array[j] > array[j + 1])
    			{
    				tmp = array[j];
    				array[j] = array[j + 1];
    				array[j + 1] = tmp;
    
    				flag = 0;	// 此段代码被执行,说明还没有排好
    			}
    		}
    
    		printf("i =  %d ,排序后的数组为:
    ",i);
    
    		for (k = 0; k < len; ++k)
    		{
    			printf("%d	", array[k]);
    		}
    
    		printf("
    ");
    	}
    }
    
    #if 1
    void main()
    {
    	int i;
    
    	//定义整型数组
    	int array[] = { 11, 8, 7, 6, 3 };
    	//计算数组长度
    	int len = sizeof(array) / sizeof(int);
    
    	//遍历数组
    	printf("待排序数组序列: ");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	//排序
    	bubbleSort(array, len);
    
    	//遍历
    	printf("冒泡排序之后的序列:	");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	system("pause");
    }
    #endif
    

    运行结果:

    3.5 冒泡总结

    • 冒泡排序是一种效率低下的排序方法,在数据规模很小时,可以采用。数据规模比较大时,最好用其它排序方法。

    • 上述例子总对冒泡做了优化,添加了exchange作为标记,记录序列是否已经有序,减少循环次数。

    3.6 稳定性

    • 冒泡排序是一种稳定的排序算法

    • 冒泡排序的效率:(O(n^2))

    4 插入排序

    4.1 算法介绍

    每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。

    4.2 基本思想

    设数组为 a[0] ~ a[n-1]

    1. 初始时,a[0] 自成 1 个有序区,无序区为 a[1] ~ a[n-1]。令 i = 1

    2. a[i] 并入当前的有序区 a[1] ~ a[i-1] 中形成 a[0] ~ a[i] 的有序区间。

    3. i++ 并重复第 2 步直到 i == n-1 。排序完成。

    4.3 算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    //插入排序算法(升序排列)
    void insertionSort(int *array, int len)
    {
    	int tmp = 0;	// 存储基准数
    	int index = 0;	// 坑的位置
    
    	int i = -1;
    	int j = -1;
    	int k = -1;
    
    	// 遍历无序序列
    	for (i = 1; i < len; ++i)
    	{
    		index = i;
    		tmp = array[i];
    
    		// 遍历有序序列(从后往前)
    		for (j = i - 1; j >= 0; --j)
    		{
    			// 基准数根有序序列中的元素比较
    			if (tmp < array[j])
    			{
    				// 有序序列元素后移
    				array[j + 1] = array[j];
    				// 坑的位置
    				index = j;
    			}
    			else
    			{
    				break;
    			}
    		}
    		// 填坑
    		array[index] = tmp;
    
    		printf("i =  %d ,排序后的数组为:
    ",i);
    
    		for (k = 0; k < len; ++k)
    		{
    			printf("%d	", array[k]);
    		}
    
    		printf("
    ");
    	}
    }
    
    #if 1
    void main()
    {
    	int i = -1;
    
    	//定义整型数组
    	int array[] = { 12, 5, 33, 6, 10 };
    	//计算数组长度
    	int len = sizeof(array) / sizeof(int);
    
    	//遍历数组
    	printf("待排序数组序列: ");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	//排序
    	insertionSort(array, len);
    
    	//遍历
    	printf("插入排序之后的序列:");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	system("pause");
    }
    #endif
    

    运行结果:

    4.4 稳定性

    • 插入排序是稳定的排序算法

    • 插入排序效率:(O(n^2))

    5 希尔排序

    5.1 算法介绍

    希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因 DL.Shell 于 1959 年提出而得名。

    5.2 基本思想

    先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

    简单来说

    希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个数组恰被分成一组,算法便终止。

    5.3 排序过程

    5.4 算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    //希尔排序
    void shellSort(int *array, int len)
    {
    	// 步长
    	int gap = len;
    
    	
    	int i = -1;
    	int j = -1;
    	int k = -1;
    
    	while (gap > 1)
    	{
    		// 步长递减公式
    		gap = gap / 3 + 1;
    
    		// 分组, 对每一组, 进行插入排序
    		for (i = 0; i < gap; ++i)
    		{
    			int tmp;	// 基准数
    			int index;	// 坑的位置
    
    			// 插入排序
    			// 无序序列
    			for (j = i + gap; j < len; j += gap)
    			{
    				tmp = array[j];
    				index = j;
    
    				// 有序序列(从后往前遍历)
    				for (k = j - gap; k >= 0; k -= gap)
    				{
    					if (tmp < array[k])
    					{
    						// 后移
    						array[k + gap] = array[k];
    						// 位置
    						index = k;
    					}
    					else
    					{
    						break;
    					}
    				}
    				// 填坑
    				array[index] = tmp;
    			}
    		}
    	}
    }
    
    #if 1
    void main()
    {
    	int i = -1;
    
    	//定义整型数组
    	int array[] = { 12, 5, 33, 6, 10 };
    	//计算数组长度
    	int len = sizeof(array) / sizeof(int);
    
    	//遍历数组
    	printf("待排序数组序列: ");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	//排序
    	shellSort(array, len);
    
    	//遍历
    	printf("希尔排序之后的序列:	");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	system("pause");
    }
    #endif
    

    运行结果:

    5.5 关于希尔排序步长的说明

    • 步长的计算公式可以自行制定,最后步长 == 1 即可。

    • 通过大量测试得出的结论:步长 = 步长 / 3 + 1

    5.6 稳定性

    • 希尔排序是不稳定的排序算法

    • 希尔排序的效率:(O(n*logn))(O(1.3*n))

    6 快速排序

    6.1 算法介绍

    快速排序是C.R.A.Hoare于1962年提出的一种 划分交换排序。它 采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

    6.2 基本思想

    1. 先从元素序列中取出一个数作为基准数(枢轴)

    2. 分区过程:将比这个数大的数全放在它的右边,小于或等于它的数全放在它的左边。(升序)

    3. 再对左右区间重复第 2 步,直到各个区间只有一个数

    6.3 算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    //快速排序
    void quickSort(int s[], int l, int r)
    {
    	if (l < r)
    	{
    		int i = l, j = r;
    		// 拿出第一个元素, 保存到x中,第一个位置成为一个坑
    		int x = s[l];
    
    		while (i < j)
    		{
    			// 从右向左找小于x的数
    			while (i < j && s[j] >= x)
    			{
    				//左移, 直到遇到小于等于x的数
    				j--;
    			}
    
    			if (i < j)
    			{
    				//将右侧找到的小于x的元素放入左侧坑中, 右侧出现一个坑
    				//左侧元素索引后移
    				s[i++] = s[j];
    			}
    
    			// 从左向右找大于等于x的数
    			while (i < j && s[i] < x)
    			{
    				//右移, 直到遇到大于x的数
    				i++;
    			}
    
    			if (i < j)
    			{
    				//将左侧找到的元素放入右侧坑中, 左侧出现一个坑
    				//右侧元素索引向前移动
    				s[j--] = s[i];
    			}
    		}
    
    		//此时 i=j,将保存在x中的数填入坑中
    		s[i] = x;
    
    		quickSort(s, l, i - 1); // 递归调用 
    
    		quickSort(s, i + 1, r);
    	}
    }
    
    #if 1
    void main()
    {
    	int i = -1;
    
    	//定义整型数组
    	int array[] = { 12, 5, 33, 6, 10 };
    	//计算数组长度
    	int len = sizeof(array) / sizeof(int);
    
    	//遍历数组
    	printf("待排序数组序列: ");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	//排序
    	quickSort(array, 0, len-1);
    
    	//遍历
    	printf("快速排序之后的序列:	");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	system("pause");
    }
    #endif
    

    运行结果:

    6.4 稳定性

    • 快速排序是一种不稳定的排序算法

    • 排序效率: (O(n*logn))

    7 归并排序

    7.1 算法介绍

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

    7.2 基本思想

    基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。

    如何让这二组组内数据有序了?

    可以将A,B 组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。

    7.3 归并的定义

    将两个或两个以上的 有序序列合并成一个新的有序序列:有序序列 V[1] ~ V[m] 和 V[m+1]~ V[n] ---> V[1] ~ V[n],这种归并方法称为 2 路归并

    7.4 如何合并两个有序序列

    7.4.1 代码实现

    // 将两个有序数列a[first...mid]和a[mid+1...last]合并。
    void mergeArray(int a[], int first, int mid, int last, int temp[])
    {
    	int leftStart = first;	//左有序序列起点
    	int leftEnd = mid;	//左有序序列终点
    	int rightStart = mid + 1;	//右有序序列起点
    	int rightEnd = last;	//右有序序列终点
    	int length = 0;	//两个有序序列合并之后的有序序列长度
    	int i = leftStart, j = rightStart;
    	
    	// 将两个有序序列中的元素合并到第三个有序序列中(a的左半部分和右半部分合并到temp中)
    	while (i <= leftEnd && j <= rightEnd)
    	{
    		// 按照从小到大的顺序放入到temp中
    		if (a[i] <= a[j])
    		{
    			temp[length++] = a[i++];
    		}
    		else
    		{
    			temp[length++] = a[j++];
    		}
    	}
    
    	// 如果左半部分还有元素, 直接放到temp中
    	while (i <= leftEnd)
    	{
    		temp[length++] = a[i++];
    	}
    
    	// 如果右半部分还有元素, 直接放到temp中
    	while (j <= rightEnd)
    	{
    		temp[length++] = a[j++];
    	}
    
    	// 将temp中排好的序列拷贝到a数组中
    	for (i = 0; i < length; i++)
    	{
    		// 只替换已排好序的那一部分
    		a[leftStart + i] = temp[i];
    	}
    }
    

    7.5 排序过程

    7.6 算法实现

    #include <stdio.h>
    #include <stdlib.h>
    
    // 将两个有序数列a[first...mid]和a[mid+1...last]合并。
    void mergeArray(int a[], int first, int mid, int last, int temp[])
    {
    	int leftStart = first;	//左有序序列起点
    	int leftEnd = mid;	//左有序序列终点
    	int rightStart = mid + 1;	//右有序序列起点
    	int rightEnd = last;	//右有序序列终点
    	int length = 0;	//两个有序序列合并之后的有序序列长度
    	int i = leftStart, j = rightStart;
    	
    	// 将两个有序序列中的元素合并到第三个有序序列中(a的左半部分和右半部分合并到temp中)
    	while (i <= leftEnd && j <= rightEnd)
    	{
    		// 按照从小到大的顺序放入到temp中
    		if (a[i] <= a[j])
    		{
    			temp[length++] = a[i++];
    		}
    		else
    		{
    			temp[length++] = a[j++];
    		}
    	}
    
    	// 如果左半部分还有元素, 直接放到temp中
    	while (i <= leftEnd)
    	{
    		temp[length++] = a[i++];
    	}
    
    	// 如果右半部分还有元素, 直接放到temp中
    	while (j <= rightEnd)
    	{
    		temp[length++] = a[j++];
    	}
    
    	// 将temp中排好的序列拷贝到a数组中
    	for (i = 0; i < length; i++)
    	{
    		// 只替换已排好序的那一部分
    		a[leftStart + i] = temp[i];
    	}
    }
    
    // 归并排序
    void mergeSort(int a[], int first, int last, int temp[])
    {
    	if (first < last)
    	{
    		// 找到数组的中间位置
    		int mid = (first + last) / 2;
    		// 左边有序
    		mergeSort(a, first, mid, temp);  
    		// 右边有序
    		mergeSort(a, mid + 1, last, temp); 
    		// 再将二个有序数列合并
    		mergeArray(a, first, mid, last, temp); 
    	}
    }
    
    #if 1
    void main()
    {
    	int i = -1;
    
    	//定义整型数组
    	int array[] = { 12, 5, 33, 6, 10 };
    	//计算数组长度
    	int len = sizeof(array) / sizeof(int);
    
    	//创建合适大小的临时数组
    	int *p = (int*)malloc(sizeof(int) * len);
    
    	if (p == NULL)
    	{
    		return;
    	}
    
    	//遍历数组
    	printf("待排序数组序列: ");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	mergeSort(array, 0, len - 1, p);
    
    	free(p);
    
    	//遍历
    	printf("归并排序之后的序列:	");
    
    	for (i = 0; i < len; ++i)
    	{
    		printf("%d	", array[i]);
    	}
    
    	printf("
    ");
    
    	system("pause");
    }
    #endif
    

    运行结果:

    7.7 稳定性

    • 归并排序是一种稳定的排序算法。

    • 排序效率: (O(n*logn))

    8 总结

    排序算法 平均时间复杂度 最坏时间复杂度 平均空间复杂度 稳定性
    选择排序 (O({n^2})) (O({n^2})) (O(1)) 不稳定
    冒泡排序 (O({n^2})) (O({n^2})) (O(1)) 稳定
    直接插入排序 (O({n^2})) (O({n^2})) (O(1)) 稳定
    希尔排序 (O({nlogn})) (O({n^2})) (O({nlog_2n})) 不稳定
    快速排序 (O({nlogn})) (O({n^2})) (O(1)) 不稳定
    归并排序 (O({nlogn})) (O({nlogn})) (O(n)) 稳定
  • 相关阅读:
    理解JavaScript中的深拷贝和浅拷贝
    再学UML-Bug管理系统UML2.0建模实例(四)
    再学UML-Bug管理系统UML2.0建模实例(三)
    再学UML-Bug管理系统UML2.0建模实例(二)
    再学UML-Bug管理系统UML2.0建模实例(一)
    再学UML-深入浅出UML类图(五)
    再学UML-深入浅出UML类图(四)
    再学UML-深入浅出UML类图(三)
    再学UML-深入浅出UML类图(二)
    再学UML-深入浅出UML类图(一)
  • 原文地址:https://www.cnblogs.com/PikapBai/p/13393325.html
Copyright © 2011-2022 走看看