日撸 Java 三百行(43 天: 插入排序:从线性结构的插入思想中启发的排序思想)

注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明

目录

一、关于排序

二、关于插入排序逻辑

三、插入排序的代码逻辑

四、代码与测试

性能与特性分析

总结


一、关于排序

        查询操作暂时告一段落,然后进入最后的排序环节。排序算法是线性结构逻辑思维的完美应用与最佳体现,这里面蕴含的一些线性结构策略,同样也可以应用于非排序问题中。

所谓排序即实现元素的重新排列使得元素保持单调性。数据的单调性能非常方便很多数据的处理,基本有序的数据能辅助相关算法的简化与适用性;例如前几日了解的折半的思想正是基于有序获得的结果,而且有序的数据能保证相同的元素紧密相邻从而极大方便了部分数据的处理过程;以及类似元素数据的分区管理,比如说花名册上的名字总是按照拼音序号有序地排列,这样我们只要知道我们的名字的拼音续就基本可以确定名字于花名册中的顺序,避免了一个个去寻找。因此,有序的顺序还非常有利于数据的管理。

        综上,研究排序就是基本数据操作与数据关联的非常重要一步。

二、关于插入排序逻辑

        排序有非常多的算法类别,大体上分为复杂排序与简单排序两大类,这两大类的排序算法各有各的应用环境、条件、复杂度和结构。今天我们讲述的将是插入排序。

        插入排序的核心思想在于划分区域而治,当然这句话并不是说是按照分治那种无数细分,这里的分区域是将整个线性结构分为“ 有序区 ”与“ 待定区域 ”,“ 有序区 ”区域是完全有序的,在有序区域插入元素让整个区域保存有序并不是一件难事,主要进行基本的顺序表插入即可,这个我在我的顺序表操作的博客中有详细阐述。

        而“ 待定区域 ”是线性表中还未纳入有序区域的元素,算法的方案是,不断从“ 待定区域 ”区域中取出一个预备元素,然后将这个预备元素按序插入到有序数列中。即算法过程:自左向右遍历依次确定一个结点\(i (2≤i≤n)\),并将结点\(i\)插入到范围为\([1,i-1]\)的数组中。

        请注意,这里我们的排序还是从下标为1开始的,我们滞留了一个空位\(L[0]\)并不参与比较,而且规定\(L[0]≤ \min (L[i])(1≤i≤n)\),代码中说将结点\(i\)插入到范围为\([1,i-1]\)的数组中,当然,如果\(L[0]\)比所有元素都小的话,那么说\(i\)插入到范围为\([0,i-1]\)的数组中似乎也没毛病。

        代码我们将采用后者。为何要多此一步呢?其实这仍然是一种哨兵思想,方便于避免遍历的过程中的越界问题。具体可见下面的分析。

        这里我们枚举一个数组: { 5, 3, 6, 10, 7, 1, 9 },我们来看下插入排序的过程(部分):

        (见上图)首先最开始数组首位5(蓝)本身只有一个元素因此默认是有序,而后的无序元素(白色)数组中我们取首元素3(橙)作为预备元素,将其插入到有序数组(蓝)中

         (见上图,插入结果)

         而后再经历若干插入后得到如此结果,这时我们继续取出无序数组\([5,7]\)的首元素7,将其作为预备元素并插入到有序组中。

         (见上图,插入结果) 

三、插入排序的代码逻辑

        上面初步演示的算法的核心流程,接下来为了更加具体说明代码思路并且讲述为什么要引入首元素的空位而且空位必须小于全部元素,接下来我先以上图中7元素的插入为例:

         (上图)首先确定元素当前要插入的元素\(L[i]\),并且用变量tempNode记录这个元素,然后令我们的指针\(j=i-1\)(就是上图的红色剪头)。

         (上图)之后不断后移剪头,让剪头在第一个小于tempNode的数据前停下。

         (上图)在下标\(j\)停下来之前,每次移动前都会通过\(L[j + 1] = L[j]\)修改元素(类似于数组插入)

         (上图)最后,将暂存tempNode的元素返回给\(L[j + 1]\)。可见暂存tempNode是为了避免\(L[j + 1] = L[j]\)操作导致的数据丢失。

        最后,我们假设插入的数据是一个有效元素中极小的一个元素:

         (上图)按照预定策略,若插入一个极小的元素,那么我们的指针\(j\)最终一定会落在\(L[0]\)处,这就保证了我们关键的操作指针\(j\)永远不会越界,避免了关于越界的条件判断。这种取舍其实就是哨兵思想的核心。

         (上图,操作的后续)

        其实只要看过严蔚敏版本的数据结构教材,这里还能省略tempNode,其实我们就可以拿\(L[0]\)来充当我们暂存数据,因为我们定义\(L[0]\)存放的是全表最小的数据,如果说\(L[i]\)的元素不是最小的那么它根本不会遍历到最后,如果说\(L[i]\)是最小的,那么自然\(L[0]\)暂存的也是最小的,我们的指针挪到这里因为发现这里的值并不比\(L[0]\)大(因为相等了),因此也能安全结束。当然真如此写代码可读性可能会弱些,对于不了解的人来说可能一遍看不懂,因此设置一个tempNode变量增加了可读性也不亏。

四、代码与测试

        今天提前把代码逻辑说了,因此今天这部分我就不废话了,直接上代码和测试:

    /**
	 *********************
	 * Insertion sort. data[0] does not store a valid data. data[0].key should be
	 * smaller than any valid key.
	 *********************
	 */
	public void insertionSort() {
		DataNode tempNode;
		int j;
		for (int i = 2; i < length; i++) {
			tempNode = data[i];

			// Find the position to insert.
			// At the same time, move other nodes.
			for (j = i - 1; data[j].key > tempNode.key; j--) {
				data[j + 1] = data[j];
			} // Of for j

			// Insert.
			data[j + 1] = tempNode;

			System.out.println("Round " + (i - 1));
			System.out.println(this);
		} // Of for i
	}// Of insertionSort

	/**
	 *********************
	 * Test the method.
	 *********************
	 */
	public static void insertionSortTest() {
		int[] tempUnsortedKeys = { -100, 5, 3, 6, 10, 7, 1, 9 };
		String[] tempContents = { "null", "if", "then", "else", "switch", "case", "for", "while" };
		DataArray tempDataArray = new DataArray(tempUnsortedKeys, tempContents);

		System.out.println(tempDataArray);

		tempDataArray.insertionSort();
		System.out.println("Result\r\n" + tempDataArray);
	}// Of insertionSortTest

性能与特性分析

        因为每个排序都有自己的性能与特性,所以在进入排序部分学习后我都额外加个这个版块来简单说一下讲述的排序特点。

        插入排序的空间复杂度是\(O(1)\),最好情况下整个数组有序(我们目标的有序),每次在新指针的第一次挪动都不会执行,即不存在指针挪动,更不存在元素挪动,复杂度估算为\(O(N)\);最糟糕的是逆有序,这种情况下每次指针都会挪动到第一个位置,比较次数大概为\(\sum_{2}^{n}i\),移动次数为\(\sum_{2}^{n}i+1\),时间复杂度估算为\(O(N^2)\)。总的来看,总的平均复杂度为\(O(N^2)\)

        同时这个算法是个稳定的算法(注:所谓稳定性,是在数组中存在某个下标关系:\(i<j\),且\(L[i] = L[j]\),而在排序后,保持\(L[i] = L[j]\)值关系下,依旧满足\(i<j\)。简单来说就是排序只改变不同数值的元素之间的关系而不会改变相同值元素之前的相对位置关系),这个特点是由顺序表插入特性决定的(指针总会第一次发现目标大小关系时停下来)。

        通过上述的最佳复杂度分析可以发现,插入排序前,若前段部分本身就有序的话,那么这部分是不存在插入操作,因此可以设想,假若整个表结构是基本有序,那么算法的优越性就体现出来了。此外,这个算法的元素挪动都是基于相邻元素的挪动,并没有突然调用某个基于下标计算确定的位置,因此无需随机存取,所以适用于顺序表和链表。若在排序中途结束排序,那么可以发现数组的前半部分将会是有序的,但是仍取一个无论有序部分还是无序部分的一个元素,我们都无法断言当前位置的元素是否是这个元素的最终位置

总结

         插入排序的核心思想是线性表的插入,算法的过程本质上也就是线性表插入的循环而已。因此对于存在插入功能的链表依旧适用,所以是链表为数不多的可以采用的一种排序方案(此外还可以用冒泡和简单选择排序)此外,在顺序表中,因为算法中存在“ 于有序结构中查询下标 ”来确定插入的过程,因此这部分可以使用二分法优化,当然这个操作只优化了查找但没优化顺序表的移动元素,因此效果很有限。

        插入排序与简单选择排序是我们所学习的最好接收的两个排序思想,因为这两个排序都从大多数计算机初学者初学的一些基础数据结构思想中诞生的。(线性表插入思想与擂台思想)

猜你喜欢

转载自blog.csdn.net/qq_30016869/article/details/124379368