数据结构学习(六) --排序
排序
排序输入是一个记录集合,输出也是一个记录集合。
排序过程中主要的两种操作:比较和移动。
概念
假设含有n个记录的序列为{r1,r2,……,rn},其相应的关键字分别为{k1,k2,……,kn},需确定1,2,……,n的一种排列p1,p2,……,pn,使其相应的关键字满足kp1≤kp2≤……≤kpn(非递减或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1,rp2,……,rpn},这样的操作就称为排序。
1 稳定性和不稳定性
一列数中,如果Ai=Aj ,且Ai排在Aj之前。经过排序算法后Ai仍然在Aj之前。这种排序算法是稳定的。相反,这种算法就是不稳定的。
2 内排序和外排序
根据排序过程中是否把所有的元素加载到内存中。可以把排序分为内排序和外排序。
内排序:排序过程中,所有的元素都加载到内存中。
外排序:记录太多,不能把所有元素加载到内存中。整个排序过程中需要在内外存之间多次交换数据才能进行。
影响内排的三个方面
(1) 时间性能
尽可能少的比较和尽可能少的移动。
(2) 辅助空间
执行算法所需要的其它存储空间。
(3) 算法的复杂性
一. 冒泡排序
(一) 定义
是典型的交换排序。两两比较相邻的记录的关键字,如果反序则交换,直到没有反序的记录为止。
(二) 算法实现
由双层循环实现,其中外层循环用于控制排序轮数,一般为要排序的数组长度减1次,因为最后一次循环只剩下最后一个数组元素,不需要对比,同时数组 已经完成排序了。而内层循环主要用于对比数组中每个邻近元素的大小,以确定是否交换位置,对比和交换次数随排序轮数而减少。
(三) 3种方式
1. 平时大家写的最多的冒泡排序
本质上不算是冒泡排序。因为不符合冒泡排序的定义。不是相邻的两两比较。而是,一个元素和之后的每个元素进行比较。如果是逆序,则进行交换。
缺点是:每次排序的过程,仅仅选择了第I个为止的正确的数值,对其余为止的数据的为止没有帮助。比如:可能将次小的数值交换到了最后。
- public int[] bubbleSort1(int[] array) {
- for (int i = 0; i < array.length - 1; i++) {
- for (int j = i + 1; j < array.length; j++) {
- if (array[i] > array[j]) {
- int tmp = array[i];
- array[i] = array[j];
- array[j] = tmp;
- }
- }
- }
- return array;
- }
2.纯正的冒泡排序
第一层控制循环的层数。第二层,从最后一个元素开始,相邻的两个元素两两比较,如果逆序,交换彼此元素的位置。
- public int[] bubbleSort2(int[] array) {
- for (int i = 1; i < array.length; i++) {
- for (int j = array.length - 1; j >=i; j--) {
- if (array[j-1] > array[j]) {
- int tmp = array[j];
- array[j] = array[j-1];
- array[j-1] = tmp;
- }
- }
- }
- return array;
- }
3.优化的冒泡排序
--如果已经完成有序了,随时中止之后的排序。从后向前,两两比较如果没有发生一次交换,说明剩余的已经有序了。中止排序。优化了一种情况:如果已经有序,及时发现并中断排序。
- public int[] bubbleSort3(int[] array) {
- boolean flag = true;
- for (int i = 1; i < array.length && flag; i++) {
- for (int j = array.length - 1; j >=i; j--) {
- flag = false ;
- if (array[j-1] > array[j]) {
- int tmp = array[j];
- array[j] = array[j-1];
- array[j-1] = tmp;
- flag = true;
- }
- }
- }
- return array;
- }
二. 简单选择排序
(1) 定义
简单排序就是通过n-1次比较,选择其中最小的记录。并和第i个元素进行交换。思路是:比较可以多次,但交换只有一次。可以理解为第一种冒泡排序的一种优化。
通过多次比较,记录最小数据的下标。然后将最小值和排序后应该保存位置的数据进行交换。
(2) 代码实现
思路是:多次比较,记录n-i个元素中最小的数据的下标。交换一次。相比冒泡减少了移动的次数。
- public class SimpleSelectSort {
- public int[] simpleSelectSort(int[] arry) {
- for (int i = 0; i < arry.length-1; i++) {
- int min = i;
- for (int j = i + 1; j < arry.length; j++) {
- if (arry[min] > arry[j]) {
- min = j;
- }
- }
- if(min!=i){
- int tmp = arry[i];
- arry[i] = arry[min];
- arry[min]= tmp;
- }
- }
- return arry;
- }
三. 直接插入排序
(1) 定义
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
理解:就是将n个待排序的元素看成一个有序表和一个无序表,开始时有序表中只有一个元素,无序表中有n-1个元素,排序过程中每次从无序表中取出第一个元素,将其插入到有序表中的指定位置,使之成为一个新的有序表,重复n-1次可完成排序过程。
代码思路:
一个n个元素的顺序存储序C列可以理解成“一个元素的一个有序序列A”和“n-1个元素的无序序列组成B”,则A=C[1],B=C[2,n-2]。从B的第一个元素开始,将B[0]的数值放在C[0]中充当哨兵,然后,这个数值和A中元素从后向前依次开始比较,如果A[i]>哨兵的数值,则A[i]向后移动一位。如果A[i]<=哨兵的数值。则循环比较退出,且将哨兵的数值放入A[i]的位置上。具体代码实现如下:
(2) 代码实现
- public int[] sort(int[] array) {
- int tmp, j = 0;
- for (int i = 2; i < array.length; i++) {
- if (array[i] < array[i - 1]) { //需要将array[i]插入有序表
- array[0] = array[i];// 设置哨兵
- for (j = i - 1; array[j] > array[0]; j--) {
- array[j + 1] = array[j];// 记录后移
- }
- array[j + 1] = array[0]; // 插入应该插入的位置
- }
- }
- return array;
- }
四. 希尔排序
(1) 基本思想
通过将待排序的元素分为若干个子序列。利用直接插入排序的思想对子序列进行排序。然后将该子序列缩小,接着对子序列进行直接插入排序。按照这种思想,直到所有元素按照关键字都有序。
希尔排序是对直接排序的一种优化。
增量(gap)的确定方式共有3种方法:
1.gap = 需排序的元素个数,每次让 gap /2;
2.取素数,每次让 gap- -,直到 gap =1;
3.gap=需排序的元素个数,每次让 gap /3 +1;
经过大神们的逐一测试,第三种方法效率更高。
(2) 具体算法实现
假设待排序的元素有 n 个,对应的关键字分别为 a1、a2、a3…….an,设 gap =4的元素为同一个子序列,则元素的关键字 a1、a5、….ai、ai+5、an-5 为一个子序列,同理,a2 、a6、….an-6 也为一个子序列,然后对同一个子序列的关键字利用直接插入排序进行排序。第一次排序完,令gap = gap /3 +1,再划分子序列并排序。依此类推,直到 gap =1,此时对整个元素进行排序。
希尔排序代码如下
- void ShellSort(SqList *L)
- {
- int i,j;
- int increment=L->length;
- do
- {
- increment=increment/3+1; /* 增量序列 */
- for(i=increment+1;i<=L->length;i++)
- {
- if (L->r[i]<L->r[i-increment]) /* 需将L->r[i]插入有序增量子表 */
- {
- L->r[0]=L->r[i]; /* 暂存在L->r[0] */
- for(j=i-increment;j>0 && L->r[0]<L->r[j];j-=increment)
- L->r[j+increment]=L->r[j]; /* 记录后移,查找插入位置 */
- L->r[j+increment]=L->r[0]; /* 插入 */
- }
- }
- }
- while(increment>1);
- }
五. 堆排序
堆排序是对选择排序的一种优化。
对是具有如下性质的完全二叉树:大顶推:每个结点值都大于或者等于其左右孩子的值。称为大顶堆。小顶堆:每个结点的值都小预定于左右孩子的值,称为小顶堆。
如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:
(1) 堆排序算法
假设利用大顶推实现堆排序。将待排序的序列构造一个大顶推,此时,整个序列的最大值在根节点。将他移走,将剩余n-1个序列重新构造成一个堆,这样会得到n个元素次大值。如此反复执行,便能得到一个有序序列。
(2) 堆排序代码
代码分两层实现。第一层是堆排序。第二层是构建大顶堆代码。
假设堆原始数据如下图所示:方便程序说明:
第一层:堆排序
代码如下:核心关注的是:(A)怎样由无序的序列构建成一个新的堆。(B)如果输出元素后,怎样调整剩余元素成为一个新的堆。
- /* 对顺序表L进行堆排序 */
- 1 void HeapSort(SqList *L)
- 2 {
- 3 int i;
- 4 for(i=L->length/2;i>0;i--) /* 把L中的r构建成一个大顶堆 */
- 5 HeapAdjust(L,i,L->length);
- 6 for(i=L->length;i>1;i--)
- 7 {
- 8 swap(L,1,i); /*将堆顶记录和当前未经排序子序列的最后一个记录交换*/
- 9 HeapAdjust(L,1,i-1); /* 将L->r[1..i-1]重新调整为大顶堆 */
- 10 }
- 11 }
第二层:
(1)构建大顶堆。思路是从下向上,从左向右,每个非叶子结点作为根节点【4->3->2->1】,将其和子树调整成大顶堆。从第一层代码中可以看到,for(i=L->length/2;i>0;i--) 输出的i刚好是4->3->2->1的顺序。每次调用HeapAdjust,最终生成一个大顶堆。解决了问题A的疑问。
(2)调整大顶堆。
根据堆是完全二叉树得知,根结点序号i和叶子结点的关系为2i,2i+1。所以,是将左右孩子相互比较,获得较大的数值,然后将较大的数值和当前堆顶元素进行比较。如果两孩子的最大值已经比堆顶元素数值小,说明添加堆顶之后仍然是大顶堆。如果两孩子最大值比堆顶元素大,则说明需要调整。将较大的数值和堆顶元素相互交换。然后在分析将原来堆顶元素换到孩子结点之后,孩子作为根节点的堆是否仍然符合大顶堆定义。直到2S>j,跳出循环。
单次调用调整大顶堆的代码如此。具体代码见下方:
- /* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, */
- /* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
- 1 void HeapAdjust(SqList *L,int s,int m)
- 2 {
- 3 int temp,j;
- 4 temp=L->r[s];
- 5 for(j=2*s;j<=m;j*=2) /* 沿关键字较大的孩子结点向下筛选 */
- 6 {
- 7 if(j<m && L->r[j]<L->r[j+1])
- 8 ++j; /* j为关键字中较大的记录的下标 */
- 9 if(temp>=L->r[j])
- 10 break; /* rc应插入在位置s上 */
- 11 L->r[s]=L->r[j];
- 12 s=j;
- 13 }
- 14 L->r[s]=temp; /* 插入 */
- 15 }
六. 归并排序
“归并”在数据结构中的定义是将两个或者两个以上的有序表组合成一个新的有序表。
归并排序:利用归并的思想实现的排序算法。假设初始序列含有n个记录。可以看成n个有序序列。每个序列的长度为1,然后两两合并,得到[n/2]下去整个程度的长度为2或者1的有序子序列;再两两合并,......,如此重复,直到得到一个长度为n的有序序列为止。这种排序的方法称之为2路归并排序。
(1) 代码思路
归并排序的思路为:将原序列按照平均分配的方法分成两个序列,s-m和m-n。然后对各自递归调用归并排序。得到两个有序序列之后,对其进行合并merge。
Merge函数思路是比较简单的。就是把一个有序序列遍历,通过比较的方式,同时从小到大遍历两个序列。分别移动A序列下边或者B序列下标的方式,从两个序列中取出当时最小的数值。直至其中一个序列数据取完,另外剩余的序列复制到最后。得到一个完成的有序序列。需要注意的点是:Merge的参数(int SR[],int TR[],int s,int m,int n)。将ST[s-m] ST[m+1-t]归并为TR[s-n]中。
(2) 归并代码实现
- /* 将SR[s..t]归并排序为TR1[s..t] */
- 1 void MSort(int SR[],int TR1[],int s, int t)
- 2 {
- 3 int m;
- 4 int TR2[MAXSIZE+1];
- 5 if(s==t)
- 6 TR1[s]=SR[s];
- 7 else
- 8 {
- 9 m=(s+t)/2; /* 将SR[s..t]平分为SR[s..m]和SR[m+1..t] */
- 10 MSort(SR,TR2,s,m); /* 递归地将SR[s..m]归并为有序的TR2[s..m] */
- 11 MSort(SR,TR2,m+1,t); /* 递归地将SR[m+1..t]归并为有序TR2[m+1..t] */
- 12 Merge(TR2,TR1,s,m,t); /* 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] */
- 13 }
- 14 }
Merge代码实现
- /* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
- 1 void Merge(int SR[],int TR[],int i,int m,int n)
- 2 {
- 3 int j,k,l;
- 4 for(j=m+1,k=i;i<=m && j<=n;k++) /* 将SR中记录由小到大归并入TR */
- 5 {
- 6 if (SR[i]<SR[j])
- 7 TR[k]=SR[i++];
- 8 else
- 9 TR[k]=SR[j++];
- 10 }
- 11 if(i<=m)
- 12 {
- 13 for(l=0;l<=m-i;l++)
- 14 TR[k+l]=SR[i+l]; /* 将剩余的SR[i..m]复制到TR */
- 15 }
- 16 if(j<=n)
- 17 {
- 18 for(l=0;l<=n-j;l++)
- 19 TR[k+l]=SR[j+l]; /* 将剩余的SR[j..n]复制到TR */
- 20 }
- 21 }
七. 快速排序
(1) 定义
快速排序的思想是:通过一趟排序将待排序的记录分割成独立的两部分。其中一部分记录的关键字比另外一部分的关键字小。则可以在这两部分的记录上继续进行上述规则排序。以达到整个序列有序的目的。
个人理解:快速排序的算法比较简单,核心在于选取一个关键字,并把序列中小于选中关键字的记录放在一边,大于关键字的序列放在另外一边。同时返回这个关键字的下标。目的是根据关键字下边分为两个序列,再次递归调用快速排序。完成这部分的代码为函数Partition来实现。
Partition函数要做,就是先选取当中的一个关键字,想尽办法将它放在一个位置,使得左侧的数据记录的关键字都小于它,右侧的数据记录的关键字都比他大。我们成这样的关键字为枢轴(pivot)
(2) 代码实现
代码分为两个函数实现。快速排序算法和partition函数两部分实现。
快速排序算法:
大致思路如下:通过partition函数将序列查分成按照枢轴左右两个排序。然后在此基础上对左右两个序列递归调用快速排序算法继续进行排序。直至不能查分为止【low>=high】。如下图,分别对低字表和高字表进行递归快速排序。
- /* 对顺序表L中的子序列L->r[low..high]作快速排序 */
- void QSort(SqList *L,int low,int high)
- {
- int pivot;
- if(low<high)
- {
- pivot=Partition(L,low,high); /* 将L->r[low..high]一分为二,算出枢轴值pivot */
- QSort(L,low,pivot-1); /* 对低子表递归排序 */
- QSort(L,pivot+1,high); /* 对高子表递归排序 */
- }
- }
关于merge函数,首先需要选中一个数据元素,作为枢轴。然后从最后一个元素开始向前遍历,如果尾部比枢轴大,high--,如果尾部元素比枢轴小,交换low和high上的元素。交换到从low向high移动,且比较low下边的元素和枢轴大小。如果low下标的元素小,则low++,则否,交换low和high元素。如果low<high【low和high不断靠近,最终会重合,未重合的情况下】,继续循环上述。
Merge代码如下:
- /* 交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置 */
- /* 此时在它之前(后)的记录均不大(小)于它。 */
- 1 int Partition(SqList *L,int low,int high)
- 2 {
- 3 int pivotkey;
- 4 pivotkey=L->r[low]; /* 用子表的第一个记录作枢轴记录 */
- 5 while(low<high) /* 从表的两端交替地向中间扫描 */
- 6 {
- 7 while(low<high&&L->r[high]>=pivotkey)
- 8 high--;
- 9 swap(L,low,high); /* 将比枢轴记录小的记录交换到低端 */
- 10 while(low<high&&L->r[low]<=pivotkey)
- 11 low++;
- 12 swap(L,low,high); /* 将比枢轴记录大的记录交换到高端 */
- 13 }
- 14 return low; /* 返回枢轴所在位置 */
- 15 }
(3) 快排优化
A. 优化选取枢轴
当前的枢轴是随机取得第一个元素。可以从low、mid、high三个位置上获取大小居中的元素作为枢轴。
B. 优化不需要的交换
数组角标为0的地方存放pivot,为哨兵。所以,在序列中始终有一个空的位置,可以通过赋值的方式把数据移动到需要的位置。赋值比交换操作稍少一点。
C. 优化小数组时的排序
快排对于数据量大一点的序列有优势。对于数据量小的序列,直接插入更快一些。所以,可以根据数据记录总数进行判断,是使用快速排序还是直接插入排序算法。直接插入和快排结果都是构成排序二叉树。这个思路和spark中是否选中sort Shuffle还是ByPass shuffle 是一致的。根据数据量选择合适算法。
D. 优化递归操作
因为第一次递归以后,变量low就没有用处了,所以可以将pivot+1赋值给low,再循环后,来一次Partition(L,low,high),其效果等同于“QSort(L,pivot+1,high);”。结果相同,但因采用迭代而不是递归的方法可以缩减堆栈深度,从而提高了整体性能。
1. public int[] bubbleSort1(int[] array) {
2. for (int i = 0; i < array.length - 1; i++) {
3. for (int j = i + 1; j < array.length; j++) {
4. if (array[i] > array[j]) {
5. int tmp = array[i];
6. array[i] = array[j];
7. array[j] = tmp;
8. }
9. }
10. }
11. return array;
12. }