数据结构学习笔记之排序

排序

  • 一、基本概念
    • 1、排序的定义
    • 2、排序算法的分类
    • 3、排序算法衡量标准
  • 二、内部排序
    • 1、插入排序
      • 1.1、直接插入排序
      • 1.2、折半插入排序
      • 1.3、希尔排序
    • 2、交换排序
      • 2.1、冒泡排序
      • 2.2、快速排序

一、基本概念

1、排序的定义

  • 排序,我是如此定义的:排序是一种操作,一种对原本无序的序列通过按关键字进行元素值交换达到增序排列或降序排列的操作

2、排序算法的分类

  • 根据数据元素是否完全在内存中分为:内部排序和外部排序。内部排序又可细分为:插入排序(直接插入排序、折半插入排序和希尔排序)、交换排序(冒泡排序、快速排序)、选择排序(简单选择排序和堆排序)、归并排序和基数排序。外部排序只有多路归并排序

3、排序算法衡量标准

  • 衡量一个排序算法是否是一个好的算法,从时间和空间复杂度以及算法稳定性进行考量。
  • 算法稳定性,指待排序表中关键字相等的元素的前后关系在经过排序操作后依旧能够保持,如 ab 经过排序后还是 ab。不具有这种性质的算法则称不具有稳定性。值得注意的时,算法稳定性作为衡量标准时,待排序的表一定是允许关键字重复的;否则,算法稳定性便没有任何意义。

二、内部排序

1、插入排序

  • 这是一种较为直观并容易实现的排序算法,基本思想是每次将一个待排序的记录按期关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。符合这种算法思想的排序算法都称为插入排序,主要有直接插入排序、折半插入排序和希尔排序

1.1、直接插入排序

  • 该算法通常将一个待排序的序列划分为三部分:
    在这里插入图片描述
  • 有序序列可以是只有一个元素的子序列,L(i) 是当前要插入到前面有序序列中的元素,通常具体操作如下:
    ① 在有序子序列中找到 L(i) 的插入位置 k;
    ② 将有序子序列从 k 开始依次后移一个位置;
    ③ 将 L(i) 复制到 L(k) 中。
  • 以序列(49,38,65,97,76,13,27,49)并以第一个 49 为有序子序列为例:
    数据结构学习笔记之排序_第1张图片
  • 实现代码:
    // 直接插入排序
    // 数组的 0 下标不存放实际元素
    void InsertSortDirectly(int a[], int len)
    {
    	int i, j;
    	for (i = 2; i <= len; i++)
    	{
    		if (a[i] < a[i - 1])
    		{
    			// 复制为哨兵
    			a[0] = a[i];
    			for (j = i - 1; a[0] < a[j]; --j)
    				a[j + 1] = a[j];
    			a[j + 1] = a[0];
    		}
    	}
    }
    
  • 该实例没有使用临时变量来作为辅助空间,如果数组从零下标开始存放元素,则需要一个临时变量作为辅助空间;故总的来说,直接插入排序的空间复杂度为常数阶O(1)。
  • 对于时间复杂度,最好情况是 O(n),即表中元素原本就有序了,只需比较一次而不用移动元素;最坏的情况是,待排序的序列是逆序的,此时比较次数达到 2+3+…+n,移动次数达到 (2+1)+(3+1)+ … +(n+1);一般情况下,总的移动次数和比较次数均约为 n2/4;故直接插入排序的时间复杂度为 O(n2)。
  • 如图示例可以看出,直接插入排序是一个稳定的算法。
  • 直接插入排序适用于顺序存储和链式存储的线性表

1.2、折半插入排序

  • 这是对直接插入排序的改进,将移动和比较分离,先进行折半查找出元素的待插入位置。
  • 代码如下:
    // 折半插入排序
    void InsertSortUndirectly(int a[], int len)
    {
    	int i, j, low, high, mid;
    	for (i = 2; i <= len; i++)
    	{
    		a[0] = a[i];
    		low = 1;
    		high = i - 1;
    		while (low <= high)
    		{
    			mid = (low + high) / 2;
    			if (a[mid] > a[0]) high = mid - 1;
    			else low = mid + 1;
    		}
    		for (j = i - 1; j >= high + 1; --j)
    			a[j + 1] = a[j];
    		a[high + 1] = a[0];
    	}
    }
    
  • 减少了比较次数,约为 O(nlog2n),此时比较次数与待排序表的初始状态无关,与元素个数有关;移动次数未变。时间复杂度仍然是平方阶O(n2)。折半插入排序在数据量不大的排序表,具有较好性能。此外,折半插入排序也是稳定的。

1.3、希尔排序

  • 也是对直接插入排序改进而来,又叫缩小增量排序
  • 其基本思想是:将待排序的表分割成若干形如L[i, i+d, i+2d, ... ,i+kd] 的 “特殊” 子表,即将相隔某个增量的记录组成子表,对各个子表进行直接插入排序,然后再对整个表进行一次直接插入排序
    数据结构学习笔记之排序_第2张图片
  • 代码实现如下:
    // 希尔排序
    void ShellSort(int a[], int len)
    {
    	int dk, i, j;
    	for (dk = len / 2; dk >= 1; dk = dk / 2) // dk 表示步长
    	{
    		for (i = dk + 1; i <= len; ++i)
    		{
    			if (a[i] < a[i - dk]) // 将 a[i] 插入有序增量子表
    			{
    				a[0] = a[i];
    				for (j = i - dk; j > 0 && a[0] < a[j]; j -= dk)
    					a[j + dk] = a[j];   //记录后移,查找插入位置
    				a[j + dk] = a[0];
    			}
    		}
    	}
    }
    
  • 空间复杂度仍然是常数阶;当 n 一定范围内,希尔排序的时间复杂度约为 O(n1.3),最坏情况为 O(n2),很明显的是希尔排序不是一个具有稳定性的算法,如图的示例也可以看出来。希尔排序仅适用于顺序存储结构的线性表——顺序表。

2、交换排序

2.1、冒泡排序

  • 冒泡排序算法采用“打擂台”的思想,用从头开始两两比较两个元素大小,若是逆序则交换,否则不做任何操作。第一趟冒泡,是用a[0] 与表中后面的元素依次比较,将最大或最小的元素冒泡到表首;第二趟,用 a[1] 与后面元素比较……依次类推,直到 n-1 趟冒泡将表排好序。
    数据结构学习笔记之排序_第3张图片
  • 代码如下:
    // 冒泡排序
    void BubbleSort(int a[], int len)
    {
    	// 标志是否发生交换
    	bool flag = false;
    	for (int i = 0; i < len - 1; i++)
    	{
    		flag = false;
    		for (int j = len - 1; j > i; j--)
    		{
    			if (a[j - 1] > a[j])
    			{
    				int t = a[j - 1];
    				a[j - 1] = a[j];
    				a[j] = t;
    			}
    			flag = true;
    		}
    		if (flag == false)
    			return;
    	}
    }
    
  • 冒泡排序的时间复杂度仍然是常数阶。对于时间复杂度,最好情况下比较 n-1 次移动 0 次,最坏情况下进行 n-1 趟排序,每趟排序进行 n-i 次比较,比较后都必须移动元素 3 次来交换元素位置,即:
    在这里插入图片描述
  • 因此平均时间复杂度为 O(n2)。
  • 在稳定性方面,冒泡排序是一种稳定的算法。

2.2、快速排序

  • 快速排序的基本思想是基于分治法的:在待排序表工[1.n]中任取一个元素pivot作为枢轴,通过一趙排序将待排序表划分为独立的两部分エ[1.k-1]和エ[k+1.n],使得[1…k-1]中的所有元素小于 pivot,L[k+1...n]中的所有元素大于等于 pivot,则 pivot放在了其最终位置L(k)上,这个过程称为一趟快速排序。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。一趟快速排序的过程就是一个交替搜索和交换的过程。
  • 代码如下:
    // 快速排序
    int Partition(int a[], int low, int high)
    {
    	int pivot = a[low];
    	while (low < high)
    	{
    		while (low < high && a[high] >= pivot) --high;
    		a[low] = a[high];
    		while (low < high && a[low] <= pivot) ++low;
    		a[high] = a[low];
    	}
    	a[low] = pivot;
    	return low;
    }
    void QuickSort(int a[], int low, int high)
    {
    	if (low < high)
    	{
    		// 划分
    		int pivotpos = Partition(a, low, high);
    		QuickSort(a, low, pivotpos - 1);
    		QuickSort(a, pivotpos + 1, high);
    	}
    }
    int main()
    {
    	int a3[] = { 49,38,65,97,76,13,27,49 };
    	cout << endl << "快速排序:";
    	QuickSort(a3, 0, 7);
    	for (int i = 0; i < 8; i++)
    		cout << a3[i] << " ";
    	return 0;
    }
    
  • 该实例的过程如下:
    数据结构学习笔记之排序_第4张图片
  • 先取第一个元素作为轴枢,从 high 往前搜索找到第一个小于轴枢的元素 27,从 low 往后搜索找到第一个大于轴枢的元素 65,将 27 与 65 互换位置:
    在这里插入图片描述
  • 接下来继续如是操作:
    数据结构学习笔记之排序_第5张图片
    在这里插入图片描述
    数据结构学习笔记之排序_第6张图片
  • 当 i==j 时,轴枢之前的元素均小于轴枢,轴枢之后的元素均大于轴枢,完成第一趟快速排序。接下来,对子表同样进行快速排序:
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

  • 在空间效率方面,快速排序用到了递归栈,其大小与递归调用的深度相关,最好时为 O(log2n),最坏时 O(n),故平均情况是 O(log2n)。
  • 时间复杂度方面与划分是否对称有关,最最坏情况下轴枢元素前后分别是 n-1 和 0 个元素,此时时间复杂度达到O(n2);因此要尽可能选择一个待排序表中中间大小的元素作为轴枢元素,有一种方法是:选取头尾和中间元素三者的中间值作为最终的轴枢元素,还有就是从待排序表中随机选取一个;理想情况下,轴枢元素前后的元素个数都不大于n/2,此时时间复杂度为O(nlog2n)。
  • 快速排序是所有内部排序算法中平均性能最优的排序算法,因为快速排序的平均情况下的运行时间与最佳情况下的运行时间很接近。
  • 在稳定性方面,快速排序算法不是一种稳定的算法。

你可能感兴趣的:(数据结构学习笔记)