前言
InsersionSort是一种很简单直观的排序方法。它本身的过程类似于我们我们玩扑克牌的时候一张张的从后面往前的调整顺序。而归并排序是另外一种典型思想的应用,divide and conquer。从表面上看起来,他们两者的联系也就在于都是排序的方法而已。如果这个时候我们把它们和常用的一个数学概念逆序联系起来,会发现他们之间有一些比较有意思的东西。
InsertionSort介绍
插入排序的过程非常简单,用通俗的语言来描述的话,则是如下的一个过程:
1. 从第二个元素开始向前比较,一直到最后一个元素。
2. 在比较的过程中,如果前面的元素比当前元素大,则将前面的元素往后面移动一位。这样直到一个元素,将目标元素放置进去。
因为整体过程比较简单,这里我们不作详细的描述。
一个参考的实现代码如下:
public static void sort(int[] a) { for(int i = 1; i < a.length; i++) { int key = a[i]; int j = i - 1; while(j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; } a[j + 1] = key; } }
这里的一个比较元素和放置的方式是将当前需要参加比较的元素放到一个key元素里,然后每次比较的时候直接将前面大的元素覆盖后面的元素。这样直到循环外面的时候将key放置进去。还有一种实现的方式就是每次发现前面元素比较大的时候就直接和前面元素交换位置。这两者实现的最终结果是一样的。
总的来说,这个排序方法的实现没什么特殊的。我们这里的介绍相当于先埋下一个伏笔。
逆序的概念
我们现在来看看逆序的概念。一般来说,如果我们有这么一个数组a[n]。在这个数组里,则一般元素排列的位置i, j表示它们所在的索引位置。如果对于i < j这两个元素,a[i] > a[j],则表示它们两个构成一个逆序。
我们来看一个例子,比如说我们有数组{2, 3, 8, 6, 1}。 对于第一个元素2来说,排在它后面的元素而且比它还小的就和它构成了一个逆序。比如2和1。3和1, 8 和6以及1都构成逆序关系。这样,我们就很自然的会引申出一个问题,如果我们需要计算一个数组里所有逆序的关系,有什么好的办法吗?
统计法
第一个我们所能想到的方法就是前面按照定义所说明的过程。既然我们每个逆序都是一个前面元素比后面元素大,那么我们就从数组的开头每个元素向后比较,如果比当前元素小,就算累加一个逆序。这样我们可以得到一个很简单的方法:
public static int inversion(int[] a) { int count = 0; for(int i = 0; i < a.length - 1; i++) { for(int j = i + 1; j < a.length; j++) { if(a[i] > a[j]) count++; } } return count; }
从算法的时间复杂度来说,它的复杂度达到了o(n^2)。
除了这个方法外,我们还有其他的办法吗?我们再看看另外一个思路。
Insersion sort
现在我们再来回顾前面insersion sort的过程。每次一个元素都要和前面进行比较,如果前面的元素比当前元素大,他们就要交换位置。而前面的元素比当前元素,这不正说明他们构成了一个逆序吗?而我们交换他们的位置就消除了一个逆序。如此看来我们排序的过程就是一个消除逆序的过程。而且我们再来看,我们这个元素和前面的元素比较,他们交换位置之后并不会影响到数组后面的元素和前面元素的逆序关系。所以,我们只要把insersion sort里交换的次数统计出来就知道有多少个逆序了。按照这种思路,我们得到另外一种实现代码:
public static int inversion(int[] a) { int count = 0; for(int i = 1; i < a.length; i++) { int key = a[i]; int j = i - 1; while(j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; count++; } a[j + 1] = key; } return count; }
从前面对insersion sort的讨论知道,它的时间复杂度也是o(n^2)。看来这里只是体现了一种思路,但是算法性能上并没有什么改进。那么,我们还有没有更好的改进呢?
归并排序
归并排序的过程主要是一种分治的思想。它首先将数组分解成两个部分,然后分别对每个部分进行排序。通过这么一个不断递归细分的过程达到排序的效果。一个典型的过程如下图:
这里只是展示了整体的排序过程。可是他们和逆序有什么关系呢?考虑到前面我们提到过,排序就是消除逆序的过程。如果每个独立的过程对逆序的计算不影响全局的话,我们可以有一个法子来处理。我们知道,在归并排序里,消除这些逆序的过程就在于merge这个步骤。而merge这里是要合并两个排好序的数组。在前面的示例里我们可以看到一个这样的场景,比如说当我们要归并数组[2, 4, 5, 7]和[1, 2, 3, 6]时。一旦前面的某个元素比后面元素大,则从该元素起到该子数组里所有的元素都和后面的元素构成逆序。而如果我们把后面的元素放入到合并后的数组中时,这些逆序都被消除了。注意到的是,这里消除的是若干个逆序而不止一个。比如说前面2, 4, 5, 7和后面数组里的1就构成了4个逆序,如果把1放到贵并的结果数组中,这些逆序就消除了。
因此,有了这些讨论之后,我们就知道该怎么来计算逆序了。只要在每次归并的时候看后面和前面元素的比较,如果后面的元素比前面元素小,则增加前面数组所有剩下元素的个数作为增加的逆序数。一个根据归并排序的修改版逆序统计方法就出来了:
public static void mergeSort(int[] array, int start, int end) { if(start < end) { int middle = start + (end - start) / 2; mergeSort(array, start, middle); mergeSort(array, middle + 1, end); merge(array, start, middle, end); } } public static void merge(int[] a, int start, int middle, int end) { int[] array = new int[end - start + 1]; int i = 0, j = start, k = middle + 1; while(j <= middle && k <= end) { if(a[j] <= a[k]) array[i++] = a[j++]; else { count += middle - j + 1; array[i++] = a[k++]; } } while(j <= middle) array[i++] = a[j++]; while(k <= end) array[i++] = a[k++]; for(i = 0; i < array.length; i++) a[start + i] = array[i]; }
要注意到的一点就是,这里几乎和归并排序的代码一样,就是在里面增加了一行: count += middle - j + 1; 这里的count我们可以在具体实现的程序里定义成一个全局的静态变量。后面只需要取这个变量就可以了。
我们从归并排序的特性说明可以知道,该算法的时间复杂度为O(nlgn)。
总结
求逆序和排序算法有着比较紧密的联系。我们排序就是一个消除逆序的过程。对于这个话题在一年前的时候就已经想讨论一番了。因为除了前面的这几种方法以外还有一种比较巧妙的办法。可惜一直没有时间去体会也没有参透。等以后有时间的时候再好好的补充总结一下。
参考材料
http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees
相关推荐
虽然插入排序在大数据量时效率不如其他高级排序算法(如快速排序、归并排序等),但它有以下优点: 1. **简单易懂**:实现逻辑直观,易于理解和实现。 2. **稳定**:相同元素的相对顺序不会改变。 3. **低内存需求**...
通过阅读和实践这些代码,你可以深入理解排序算法的内部机制,为进一步学习更复杂的排序算法如快速排序、归并排序等奠定基础。同时,这些基础知识对于提升编程能力,优化数据处理效率,解决实际问题都有着重要作用。
为了提高效率,可以考虑对小数组使用插入排序,而对于大数组,可以结合其他更高效的算法,例如快速排序、归并排序或堆排序。此外,插入排序在最佳情况下(已排序数组)的时间复杂度为O(n),最坏情况下(逆序数组)为...
- 虽然插入排序在处理大数据集时效率不如其他高级排序算法(如快速排序、归并排序等),但在小规模数据或部分有序的数据中,插入排序有很好的性能。 - 在实际编程中,插入排序也常用于其他算法的组成部分,比如...
此外,可以考虑结合其他排序算法,如快速排序或归并排序,来创建一种混合策略,以适应不同的输入特性。 在MATLAB中,除了插入排序,还有许多内置的排序函数,如`sort`和`sortrows`,它们在处理大规模数据时更为高效...
void insertionSort(int arr[], int n) { for (int i = 1; i ; i++) { int key = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } // 归并...
对于大规模无序数据,更高效的算法如快速排序、归并排序或堆排序会是更好的选择。然而,插入排序的一个优点是它的稳定性——相等的元素在排序后不会改变它们的相对顺序。此外,由于其简单性,插入排序也是教学和理解...
5. **归并排序**(Merge Sort):归并排序同样基于分治思想,将数组分成两个子数组,分别进行排序,然后合并两个已排序的子数组,保证最终结果的有序性。 6. **希尔排序**(Shell Sort):希尔排序是插入排序的优化...
插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理是通过构造一个有序序列,对于未排序数据,在已...在实际应用中,对于大数据量的排序,通常会选择更高效的算法,如快速排序、归并排序或堆排序等。
在实际应用中,插入排序通常用于小规模数据或者作为其他高级排序算法(如快速排序、归并排序)的基石,特别是在处理部分有序的数据时,它的表现往往优于其他O(n^2)的排序算法。同时,插入排序也可以用于构建更复杂的...
7. **归并排序(Merge Sort)** 归并排序也采用分治法,将数组拆分成两半,分别排序,然后再合并。无论输入数据的顺序如何,归并排序都能保证O(n log n)的时间复杂度,但需要额外的存储空间。 8. **基数排序...
- 对于大数据量,可以考虑结合其他高效的排序算法,如快速排序、归并排序等。 9. **代码实现**: 从提供的`InsertionSort.cpp`文件中,我们可以期待看到一个用C++实现的插入排序。通常,C++代码会包含一个名为`...
理解并掌握其工作原理,有助于进一步学习和理解更复杂的排序算法,如快速排序、归并排序等。通过阅读"算法-理论基础- 排序- 直接插入排序(包含源程序).pdf"文件,你可以深入了解其细节并实践代码,提升编程能力。
5. **归并排序(Merge Sort)**: 归并排序是基于分治策略的排序算法,将数组分成两半,分别对每一半进行排序,然后将两个有序的子数组合并成一个完整的有序数组。它的时间复杂度始终为O(n log n),稳定性好。 6. ...
排序算法是计算机科学中至关重要的一部分,它涉及到如何有效地组织数据以提高处理效率。...在实际开发中,还可以考虑其他高效的排序算法,如归并排序、堆排序等,它们在特定条件下可能提供更好的性能。
void InsertionSort(int arr[], int n) { for (int i = 1; i ; i++) { int key = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } ``` ###...
6. **归并排序(Merge Sort)**: 归并排序也是基于分治策略,将数组递归地分成两半,分别排序,然后合并。其时间复杂度始终为O(n log n),但需要额外的O(n)空间。 7. **堆排序(Heap Sort)**: 堆排序利用了...
在实际应用中,虽然对于大规模数据的排序效率不如更高级的算法,如快速排序、归并排序等,但它的实现简单,适合小规模或部分有序的数据排序。 在直接插入排序中,我们首先假设数组的第一个元素已经是排序好的,然后...
5. 归并排序(Merge Sort):也是基于分治策略,将数组分为两半,分别排序后再合并,保证了稳定性和O(n log n)的时间复杂度。 6. 堆排序(Heap Sort):利用堆这种数据结构,将待排序序列构造成一个大顶堆或小顶堆...
3. 归并排序(Merge Sort): 归并排序是分治法的一个典型应用,它将大问题分解成小问题来解决。首先将数组分为两半,分别对这两半进行排序,然后将两个已排序的部分合并成一个有序数组。在C++中,归并排序一般会...