图解数据结构---排序

🌞欢迎来到图解数据结构的世界 
🌈博客主页:卿云阁

💌欢迎关注🎉点赞👍收藏⭐️留言📝

🌟本文由卿云阁原创!

📆首发时间:🌹2024年8月7日🌹

✉️希望可以和大家一起完成进阶之路!

🙏作者水平很有限,如果发现错误,请留言轰炸哦!万分感谢!


目录

排序的基本概念

插入排序

希尔排序(ShellSort)

冒泡排序

快速排序

选择排序

堆排序

 堆的插入和删除操作

​编辑

 归并排序

基数排序

外部排序

 败者树

置换-选择排序

最佳归并树

排序的基本概念

 

排序的概念:让关键字有序(关键字是可以重复的)

排序算法的评价指标:

时间复杂度,空间复杂度,算法的稳定性

(相同的关键字的相对位置是否发生变化,稳定的排序算法不一定比不稳定的好,要看实际的需求)

排序算法的分类

内部排序:数据全部放在内存(时间、空间复杂度)

外部排序:数据太多,无法全部放在内存(读写磁盘的次数)

(比如我们自己定义的数组,就是放在内存中的,对于内部排序来说,排序都是在内存中进行的,内存是一个高速的设备,所以我们更关注时间,空间复杂度,对于外部排序我们还需要考虑读写磁盘的次数)


插入排序

算法思想:待排记录(按照关键字的大小)插入到有序序列

  • 49是当前的有序序列,38为待查元素,其余为无序序列
  • 处理38这个元素,把38与有序序列进行对比,比38大的元素都向后移动

  •  处理65这个元素,65>49,把65放回原来的位置。

  • 处理97这个元素,97>65,把97放回原来的位置。

  •  处理76这个元素,76<97,97后移,76>65,所以76插入到65的后面。

  •  处理13这个元素,13<97, 97后移,13<76,76后移,13<65,65后移,13<49,49后移,13<38,38后移。

  • 处理27这个元素,27<97, 97后移,27<76, 76后移,27<65,65后移,27<49,49后移,27<38,38后移。

  • 处理49这个元素,比它大的元素全部右移,但是和它相等的元素不移动,这样做可以保证算法的稳定性。

代码实现

     假设此时我们需要排序的是A[ ],有n个数据元素,变量i表示待排序的元素。

     如果当前处理的元素A[i]小于A[i-1],需要移动前面的元素,用中间变量temp保存当前待排元素的质A[i],从i的左边j(i-1),这个元素开始,检查它前面的元素A[j],是否比temp大,所有比它大的元素都需要向右移动,当j小于0,或者在有序序列中A[j]找到第一个小于temp的值,此时的A[j+1]就是temp的位置。

#include<stdio.h> 
//函数声明
void InsertSort(int A[], int n);
void print(int m, int A[], int n);
//直接插入排序
void InsertSort(int A[], int n) 
{   
    int i, j, temp;
    for (i = 1; i < n; i++)
    {   
        print(i, A, n);
        if (A[i] < A[i - 1])
        {
            temp = A[i]; 
            for (j = i - 1; j >= 0 && A[j] > temp; j--)
            {
                A[j + 1] = A[j];
            }
            A[j + 1] = temp;
        }
    }
}
//输出每次排序的结果 
void print(int m, int A[], int n) 
{   
    int i;
    printf("第%d轮:", m);
    for (i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
}
int main()
{
    int A[8] = {49, 38, 65, 97, 76, 13, 27, 49};
    int i;
    printf("初始 :");
    for (i=0; i<8; i++)
    {
        printf("%d ", A[i]);
    }
     printf("\n");
    InsertSort(A, 8);
    return 0;
}

 算法实现(带哨兵)

       实际存储的位置是从1开始的,0号位置会作为哨兵,和之前不一样的是我们会把待插入元素放入到哨兵的位置。

优点:不需要每次循环都判断j>=0。

//直接插入排序(带哨兵) 
void InsertSort(int A[], int n) 
{   
    int i, j, temp;
    for (i = 2; i < n; i++)
    {   
        if (A[i] < A[i - 1])
        {
            A[0] = A[i]; 
            for (j = i - 1;  A[j] > A[0]; j--)
            {
                A[j + 1] = A[j];
            }
            A[j + 1] = A[0];
        }
    }
}

算法效率分析

空间复杂度:O(1)

辅助变量i,j,temp(A[0])都是常数级的和n无关。

时间间复杂度:

     在进行插入排序的时候,我们都是从第2个元素开始依次往后处理,每个元素都需要进行依次for循环,我们总共需要n-1次处理,每趟都需要进行关键字的对比,可能需要进行元素的移动

平均时间复杂度=1/2(最好时间复杂度+最坏时间复杂度)

 优化--折半插入排序

    之前,当我们在处理一个元素的时候,都是采用顺序查找的方式找到它的位置,现在我们可以使用折半查找到相应的位置再移动元素

    当前处理55,A[0]=55,mid指向的值50<55,55应该插入50的右边,70>55,55应该插入70的左边,60>55,55应该插入60的左边。

当low>high,将[low,i-1]右移,把A[0]的位置放到low所指向的位置。

处理60这个元素,50<60, 右边,60<=60,元素相等,为了保证稳定性,继续在右边查找,

70>60, 左边,low>high停止折半查找。,将[low,i-1]右移,把A[0]的位置放到low所指向的位置。

    比起直接插入排序,折半插入排序减少了关键字的比较次数,但是移动元素的次数不变,时间复杂度O(n2)。

代码实现:

#include<stdio.h> 
//函数声明
void InsertSort(int A[], int n);
void print(int m, int A[], int n);
//折半插入排序(带哨兵) 
void InsertSort(int A[], int n) 
{   
    int i, j, low,high,mid;
    for (i = 2; i <= n; i++)
    {   
	    low=1;high=i-1;
        A[0]=A[i];
        while(low<=high)
        {
        	mid=(low+high)/2;
        	if(A[mid]>A[0])
        	    high=mid-1;
        	else
        	    low=mid+1;
		}
		for(j=i-1;j>=low;j--)
		    A[j+1]=A[j];
		A[low]=A[0];
		print(i-1, A, n);
    }
}
//输出每次排序的结果 
void print(int m, int A[], int n) 
{   
    int i;
    printf("第%d轮:", m);
    for (i = 1; i <=n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
}
int main()
{
    int A[12] = {999,20, 30, 40, 50, 60, 70, 80, 55, 60, 90, 10};
    int i;
    printf("初始 :");
    for (i=1; i<12; i++)
    {
        printf("%d ", A[i]);
    }
     printf("\n");
    InsertSort(A, 11);
    return 0;
}

对链表进行插入排序

  当我们要移动一个元素的时候,只要移动几个指针就可以了,但是关键字的对比仍然是O(n2)这样一个数量级。


希尔排序(ShellSort)

(代码考察频率不高)

 通过上面的分析我们可以发现基本有序的时候,直接插入排序的效率较高。

希尔排序的思想:先追求表中元素的部分有序,再逐渐逼近全局有序。

我们会把表分割成很多的子表,在每一趟的处理中,我们都会设置一个增量d,把表分成类似各个子表,对子表进行直接插入排序,直到d=1为止。

第一趟:我们把距离为d=4的元素看成是一个子表,对各个子表进行直接插入排序,

第二趟:d2=d1/2=2(希尔本人建议将增量减小一半)

子表1,直接插入排序

子表2,直接插入排序

第三趟:d3=d2/2=1

子表1,直接插入排序

 代码实现

变量d表示每一次的增量,初始状态下它的值等于d=(n/2),最后的状态是d=1,d的变化是(d=d/2)

变量i表示每个子表的最后元素的位置,初始状态下i=d+1,最后的状态是i=n,i的变化是(i++,处理下一个子表)

第一趟:d=4

子表1:i=d+1指向76,76(A[i])>49(A[i-d]),不需要进行移动。

子表2:i++指向13,13(A[i])<38(A[i-d]),把13A[i]放入A[0]j=i-d(j表示该表倒数第二个元素的位置),38A[j]后移到j+d的位置,j=j-d(检查子表前面是否还有元素,j=j-d=-1),此时j<0,跳出for循环。

子表3:类似,当i=n是进行最后一次处理

代码实现

#include<stdio.h> 
//函数声明
void InsertSort(int A[], int n);
void print(int m, int A[], int n);
//希尔排序 
void ShellSort(int A[], int n) 
{   
   int d,i,j;
   for(d=n/2;d>=1;d=d/2) 
   {
   	for(i=d+1;i<=n;i++)
       {
       	if(A[i]<A[i-d])
       	{
       		A[0]=A[i];
       		for(j=i-d;j>0&&A[0]<A[j];j=j-d)
       		    A[j+d]=A[j];
       		A[j+d]=A[0];
		}
	   }
	print(d, A, n);
   }
       
}
//输出每次排序的结果 
void print(int m, int A[], int n) 
{   
    int i;
    printf("d=%d:", m);
    for (i = 1; i <= n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
}
int main()
{
    int A[9] = {999, 49, 38, 65, 97, 76, 13, 27, 49};
    int i;
    printf("初始 :");
    for (i=0; i<9; i++)
    {
        printf("%d ", A[i]);
    }
     printf("\n");
    ShellSort(A, 9);
    return 0;
}

算法性能

空间间复杂度:O(1)

最坏时间复杂度:O(n2)

稳定性:不稳定的

适用性:仅用于顺序表


冒泡排序

算法思想 

从前往后两两比较相邻元素的值,如果为逆序(A[j]>A[j+1]),,就交换他们。

算法流程:(从前开始)

第一趟:

      49<38,交换,49<65,不需要交换,65<97,不需要交换,97>76,交换,97>13,交换,97>27,交换,97>49,交换。

                                  97这个最大的元素放到了后面

代码实现:

       j和j+1分别指向当前需要对比的两个元素,if(A[j]>A[j+1]),交换位置。

第1趟:

j的值应该是从    [0-------n-2]

第2趟:

j的值应该是从    [0-------n-3]

-----

每一趟都会确定一个最大值,n个元素要进行n-1轮的对比,我们用变量i来表示是第i趟。

i的取值应该是从    [1-------n-1]

j的取值应该是从    [0-------n-i-1]

#include<stdio.h> 
#include<stdlib.h> 
//函数声明
void InsertSort(int A[], int n);
void print(int m, int A[], int n);
void  Swap(int &a,int &b);
//冒泡排序 
void BubbleSort(int A[], int n) 
{   
    int i,j;
    for(i=1;i<=n-1;i++)
    {   
        bool flag=false; 
    	for(j=0;j<=n-i-1;j++) 
            if(A[j]>A[j+1])
            {
            	flag=true;
            	Swap(A[j],A[j+1]); 
			}
        if(flag==false) 
            return ;
        print(i, A, n);
	}
	       
}
//定义交换函数
void  Swap(int &a,int &b)
{
	int temp;
	temp=a;
	a=b;
	b=temp; 
}
//输出每次排序的结果 
void print(int m, int A[], int n) 
{   
    int i;
    printf("第%d趟:", m);
    for (i = 0; i <n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
}
int main()
{
    int A[8] = {49, 38, 65, 97, 76, 13, 27, 49};
    int i;
    printf("初始:");
    for (i=0; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    BubbleSort(A, 8);
    return 0;
}

实际上:第5趟就可以保证有序了。

算法性能分析

空间复杂度是:O(1)

时间复杂度:

最好情况(有序):

        我们进行第一趟冒泡排序,从第一个位置开始检查,发现所有的元素都不需要交换位置,算法直接结束。比较次数是n-1,交换次数是0次,时间复杂度O(n)。

最坏情况(逆序):(每次交换都需要移动元素3次)

                    比较次数是=交换次数,时间复杂度O(n2)

平均情况:O(n2)

稳定性:稳定的

适用性:顺序表和链表都可以


快速排序

算法思想:

    一次划分只能确定一个元素的最终位置,但是一趟排序,可以确定多个元素的最终位置。
      用ij分别指向这个表的头和尾的位置,选择49作为基准元素,让ij指针开始向中间移动,保证i的右边大于49,j的左边小于49。接下来对左右子表再进行划分   

 

 代码实现:

我们定义了一个数组A[ ],此时需要排序的范围是[0-----7],QuickSort(A,0,7)

low小于high,进入if函数的处理。

int p=Partition(A,low,high),这个函数实现的事情是进行一次划分。

 进入Partition函数处理      Partition(A,0,7)

int pivot=A[low]     把第一个元素49作为基准值。

while(low<high)     进入while循环

(比较)检查low<high,A[high]>=pvot,high--    这个while循环要做的是,找到一个比基准值更小的元素,跳出该循环。

        (移动)A[low]=A[high]  把high指向的值放到low指向的位置    把27放到0号位置

(比较)检查low<high,A[low]<=pvot,low++   这个while循环要做的是,找到一个比基准值更大的元素,跳出该循环。

        (移动)A[high]=A[low]  把low指向的值放到high指向的位置    把65放到6号位置

low<high 继续进行大循环

直到low大于high,不满足low<high,跳出内层的第一个循环。由于此时low=high,A[low]=A[high],相当于什么也没做,不满足low<high,跳出内层的第二个循环。由于此时low=high,A[high]=A[low],相当于什么也没做。不满足low<high,跳出整个外循环。

          A[low]=pivot        把基准位置放到,low的位置,此时我们就实现了一次划分。返回low的值。

      第一层的Partition函数执行结束,回到QuickSort函数的位置,最终以3为基准划分成左右两个部分,接下来让函数继续执行,

      接下来我们先处理左子表 QuickSort(A,low,p-1)  QuickSort(A,0,2),进入第二层的QuickSort函数。第一层的QuickSort执行到97行,调用Partition(A,0,2),

#include<stdio.h> 
//函数声明
void InsertSort(int A[], int n);
void print(int m, int A[], int n);
void QuickSort(int A[],int low,int high); 
int Partition(int A[],int low,int high);
//快速排序 
void QuickSort(int A[],int low,int high) 
{   
    if(low<high)  
	{
		int p=Partition(A,low,high);
		QuickSort(A,low,p-1);
		QuickSort(A,p+1,high);
	 } 
}
//定义函数
int  Partition(int A[],int low,int high)
{
	int p=A[low];
	while(low<high)
	{
		while(low<high&&A[high]>=p) high--;
		A[low]=A[high];
		while(low<high&&A[low]<=p) low++;
		A[high]=A[low];
	}
	A[low]=p;
    return low;
}
int main()
{
    int A[8] = {49, 38, 65, 97, 76, 13, 27, 49};
    int i;
    printf("初始 :");
    for (i=0; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    QuickSort(A, 0,7);
    printf("排序后 :");
    for (i=0; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

算法效率分析

时间复杂度:O(n*递归深度)

空间复杂度:O(递归深度)

二叉树的层数就是递归的层数

 最好的情况是划分的区域是均匀的,所以如果初始序列有序或者逆序,则效率最差。

 稳定性:不稳定


选择排序

算法原理:

每一趟排序选择关键字最小的元素,加入有序序列。对于n个元素,需要进行n-1次处理

 代码实现:

#include<stdio.h> 
//函数声明
void SelectSort(int A[],int n);
void Swap(int &a,int &b); 
void print(int m, int A[], int n);
//简单选择排序 
void SelectSort(int A[],int n) 
{   
    int i,j,min;
    for(i=0;i<n-1;i++)
    {
    	min=i;
    	for(j=i+1;j<n;j++)
    	    if(A[j]<A[min])
    	        min=j;
    	if(min!=i)
    	    Swap(A[i],A[min]);	
    	print(i+1,A,n);
	}
}
//定义函数
void Swap(int &a,int &b)
{
    int temp;
    temp=a;
    a=b;
    b=temp;
}
//输出每次排序的结果 
void print(int m, int A[], int n) 
{   
    int i;
    printf("第%d趟:", m);
    for (i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
}
int main()
{
    int A[8] = {49, 38, 65, 97, 76, 13, 27, 49};
    int i;
    printf("初始 :");
    for (i=0; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    SelectSort(A,8); 
    return 0;
}


堆排序

这种排序算法的实现基于一种叫的数据结构。

大根堆:根>=左右

小根堆:左右<=根

对于一个初始序列,如何把它建立成大根堆?

       我们要保证所有根节点的值大于左右孩子-----检查所有非终端结点,不满足则进行调整(i<=n/2向下取整)。

      下面这个例子中有8个节点,i<=4的节点是非终端结点,1,2,3,4。

       从后往前处理这些结点,第一个被处理的是4号结点,根据它的下标找到左孩子(2i=8),右孩子(2i+1=9),32>9,与大的孩子互换。

处理3号结点,87>78,互换。

处理2号结点,17<32,17<45,17与大的孩子45互换。

处理1号结点,53<87,53与大的孩子87互换。

53发生下坠,相同方法继续调整。(小元素不断下坠)

代码实现:

BuildMaxHeap(int A[ ],int len)

我们需要从n/2开始依次往后处理,把所有的分支结点,都调整为大根堆HeapAust

                                  for(i=len/2;i>0;i--)      HeapAdjust(A,i,len) 

HeapAdjust(int A[ ],int k,int len)

现在处理53这个结点

第一步:当前处理结点的值保存在A[0]---A[0]=A[k]

第二步:i指向左右孩子较大的元素

左孩子的位置:i=2*k                        右孩子的位置:i+1 

比较左右孩子的大小(A[i]       A[i+1]))     如果右孩子更大,让i++,此时i指向的是更大的孩子。只有当i<len的时候才能保证i是有右兄弟的。

第三步:

        如果当前结点A[0]的值大于A[i],说明当前的结点满足(根大于左右的),说明这个结点不需要处理跳出整个for循环。(k是有可能插入53的位置)

      如果当前结点A[0]的值小于A[i],说明当前的结点需要调整,

                                        A[k]=A[i]           把最大的孩子放到双亲结点

                                            k=i                待确定是什么元素的位置

                                           i=i*2               更新i的值,指向k的左孩子

      i=i*2               更新i的值,指向k的左孩子

 

 第一个if语句       让i指向最大的孩子,A[i]>A[0],A[k]=A[i],k=i

i=i*2>len,说明这个位置没有左右孩子了,A[k]=A[0]

#include<stdio.h> 
//函数声明
void BuildHeap(int A[],int len);
void HeapAdust(int A[],int k,int len);
//建立一个大顶堆 
void BuildHeap(int A[],int len) 
{
	int i;
	for(i=len/2;i>0;i--)
	    HeapAdust(A,i,len);
}
void HeapAdust(int A[],int k,int len)
{
	A[0]=A[k];
	int i;
	for(i=2*k;i<=len;i=i*2)
	{
		if(i<len&&A[i+1]>A[i])
	        i++;
	    if(A[0]>=A[i])
	        break;
	    else
	    {
	    	A[k]=A[i];
	    	k=i;
		}
	}
	A[k]=A[0];	    
}
int main()
{
    int A[9] = {999, 53, 17, 78, 9, 45, 65, 87,32};
    int i;
    printf("初始 :");
    for (i=1; i<9; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    BuildHeap(A,8); 
    printf("建立大顶堆 :");
    for (i=1; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    return 0;
}

基于大根堆进行排序

       每一趟将堆顶元素加入到有序子序列(与待排序列最后一个元素交换)

第一趟:

  • 把最大的元素移动到末尾
  • 再次调整为大根堆(小元素不断的下坠,len-1)

代码实现:

BuildHeap(A,len)                 建立一个大根堆 

刚开始i的值=len,   指向我们的堆底的元素。

Swap(A[i],A[1])        把堆顶元素和堆底元素进行交换

HeapAdjust(A,1,i-1)  调整成大顶堆

#include<stdio.h> 
//函数声明
void BuildHeap(int A[],int len);
void HeapAdjust(int A[],int k,int len);
void HeapSort(int A[], int len); 
void Swap(int &a,int &b);
//建立一个大顶堆 
void BuildHeap(int A[],int len) 
{
	int i;
	for(i=len/2;i>0;i--)
	    HeapAdjust(A,i,len);
}
void HeapAdjust(int A[],int k,int len)
{
	A[0]=A[k];
	int i;
	for(i=2*k;i<=len;i=i*2)
	{
		if(i<len&&A[i+1]>A[i])
	        i++;
	    if(A[0]>=A[i])
	        break;
	    else
	    {
	    	A[k]=A[i];
	    	k=i;
		}
	}
	A[k]=A[0];	    
}
void HeapSort(int A[], int len)
{
    BuildHeap(A, len);
    int i;
    for(i = len; i > 1; i--)
    {
        Swap(A[i], A[1]);
        HeapAdjust(A, 1, i-1);
    }     
}
void Swap(int &a,int &b)
{
	int temp=a;
	a=b;
	b=temp;
}
int main()
{
    int A[9] = {999, 53, 17, 78, 9, 45, 65, 87,32};
    int i;
    printf("初始 :");
    for (i=1; i<9; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    BuildHeap(A,8); 
    printf("建立大顶堆 :");
    for (i=1; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    HeapSort(A,8);
    printf("\n");
    printf("堆排序之后 :");
    for (i=1; i<8; i++)
    {
        printf("%d ", A[i]);
    }
    return 0;
}

算法效率分析

A[i]>A[i+1]   找最大的孩子,进行1次关键字的对比

A[0]>A[i]       进行1次关键字的对比

所以一个结点要有左右孩子的话,需要对比两次关键字,如果只要左孩子只要进行1次关键词的对比。


 堆的插入和删除操作

 假设我们要插入的值是13,首先把它放到表尾的位置。

找到父节点32,13<32,13上升一层,13<17,上升, 13>9符合要求,总共比较了3次。

如果要插入46这个元素,只需要进行一次关键字的对比。

 删除13这个元素

 让堆底元素代替

 46不断的下坠


 归并排序

把两个或者多个已经有序的序列合成一个

i指向的值<j指向的值(比较),把7放入k的位置(赋值),j和k的指向向后移(指向后移)

 12>10,10放进1号,j和k的指向向后移。

12<21,12放进2号,i和k的指向向后移。

16<21,16放进3号,i和k的指向向后移。

后面的操作类似,直到有边的数组已经遍历完, 直接把剩余的元素加入到总表

 "2"路归并

每次选出一个小元素需要对比1次。

 "4"路归并

每次选出一个小元素需要对比3次。

归并排序手算模拟

核心:把数组内两个有序序列归并成一个。

代码实现:目的是和并[low-mid]和[mid+1,high]这两个子序列

定义1个辅助数组B和A的大小是相同的:

        int *B=(int *)malloc(n*sizeof(int))

把[low-high]内的元素复制到B数组

        for(k=low;k<=high;k++)

                 B[k]=A[k]

 i的取值是【low-mid】

 j的取值是【mid+1-high】

for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++)

   if(B[i]<=B[j])

          A[k]=B[i++];  //如果值相等优先选择i指向

  else

         A[k]=B[j++];

 

j超过high所指向的范围,跳for循环,说明第二个数组已经合并完了。

用两个while循环来检查是否合并完。没有合并完就把剩余的元素放到总表中。

 while(i<=mid)   B[k++]=A[i++];

 while(j<=high)   B[k++]=A[j++]

 

完整代码:

对A[ ]进行归并排序,范围是[low,high]

计算mid,拆分成两个子序列[low,mid],[mid+1,high]

             int mid=(low+high)/2;

对左半部分和右半部分递归的进行排序

MergSort(A,low,mid)

MergSort(A,mid+1,high)

然后进行归并

Merg(A,low,mid,high)

算法效率分析

树高是h,需要进行h-1趟排序


基数排序

 我们要把它排成递减的序列

      所有的关键字都可以拆分成3个部分(百位   十位     个位)取值只有可能是(0--9),所以我们建立了10个辅助队列。

第一趟:以个位进行分配,比如520。

 第一趟:收集

第二趟:以十位进行分配。

 第二趟:收集

第三趟:以百位进行分配。 

 第三趟:收集

 

基数排序的应用

 


外部排序

外部排序原理

采用归并排序的思想和方法

1.数据初始状态

2.将(36、8、26)(42、9、48)分别存入输入缓冲区1、输入缓冲区2 

 3.将输入缓冲区1和输入缓冲区2的数据进行递增排序

4.将输入缓冲区1和输入缓冲区2的数据通过输出缓冲区逐一写入外存,形成一个有序归并段

5.同理进行后序的操作 

 6.一共构造了8个有序的归并段。总共需要进行16次读操作和16次写操作

7.第一次归并:读入归并段1和归并段2中的第一块磁盘(相对最小),进行排序。

 8.依次找出这两个输入缓冲区中最小元素,并将其移动到输出缓冲区中,当输出缓冲区满,则写入外存(1、8、9)    

9.继续找出这剩余元素中的最小元素,直到某一个缓冲区中空,则读入其所属归并段的后一个内存块的数据,并继续进行上述操作。直到两个缓冲区都空,且归并段1和归并段2中的元素全部读入内存,此时归并段1和归并段2就得到了一个有序的递增序列。

输入缓冲区1空,输入归并段1的第二块内存 

 排序完成,归并段1和归并段2递增有序

10.对剩余的六个归并段进行上述操作,八个归并段→四个归并段 

11.第二次归并:继续采用此方法依次取出归并段1和归并段2的各个块进行排序操作,四个归并段→两个归并段    原归并段1、2排序形成归并段1。

原归并段3、4排序形成归并段2 

 

 12.第三次归并:继续排序归并段1、2,形成最后的有序递增序列

时间开销分析

上述外部排序中:形成初始归并段→第一次归并(8 ~ 4)→第二次归并(4 ~ 2)→第三次归并(2 ~ 1)(每个过程都需要读和写16次,共32 + 32 * 3 = 128次)

总时间开销 = 内部排序所需时间 + 内部归并所需时间 + 外部读写所需时间

 优化——多路归并

改用四路归并:初始化归并段→第一次归并(8 ~ 2)→第二次归并(2 ~ 1)

需要读写次数:32 + 32 * 2 = 96

但是,与此同时,缓冲区的数量也要变成四个(k路归并需k个缓冲区)

结论:1.对于 r 个初始归并段进行 k 路归并,需要归并趟数 = (向上取整,归并树高度)
2.提升外部排序的速度、减少读写磁盘的速度的方法:提高 k 值,降低 r 值。

提高 r 值:增加归并段长度

但是,提高 k 有负面影响:

A.需要的缓存空间升高(k路归并需k个缓冲区)

B.内部归并的所需时间提高(选出最小关键字需要进行k - 1次比较)

 

 降低 r 值

 

 


 败者树

败者树的构造

 

如果此时的冠军突然弃赛,突然有别的选手参加,如何找新的冠军?

 视为一棵完全二叉树

1.将每个归并段的第一个元素作为叶子结点加入败者树中

 2.从左至右、从上往下的更新分支节点的信息:判断其左右子树的大小,除了根节点(最上面那个结点)记录冠军来自哪个归并段外,其余各分支节点记录的是失败者来自哪个归并段

 

 3.取出最小的元素1后,从其所属的归并段中取出下一个元素6,依次与从叶子结点到根节点的各个结点所记录的败者信息进行对比。

  引进败者树后,选出最小的关键字,仅需log2k次比较(向上取整)


置换-选择排序

1.初始状态

归并段1: 

2.4、6、9依次加入内存工作区中,(4、6、9)选择最小的元素4,输出4并更改MIN = 4

3.加入7,(7、6、9)选择最小元素6 > MIN = 4,输出6并更改MIN = 6

4.加入13,(7、13、9)选择最小元素7 > MIN = 6,输出7并更改MIN = 7

5.加入11,(11、13、9)选择最小元素9 > MIN = 7,输出9并更改MIN = 9

6.加入16,(11、13、16)选择最小元素11 > MIN = 9,输出11并更改MIN = 11

8.加入14,(14、13、16)选择最小元素13 > MIN = 11,输出13并更改MIN = 13

9.加入10,(14、10、16)选择最小元素10 < MIN = 13,标记13为不可输出,选择第二小的元素14 > MIN = 13,输出14并更改MIN = 14

10.加入22,(22、10、16)选择最小元素16  > MIN = 14,输出16并更改MIN = 16

11.加入30,(22、10、30)选择最小元素22 > MIN = 16,输出并更改MIN = 22

12.加入2,(2、10、30)选择最小元素2 < MIN = 22,标记2为不可输出,选择第三小的元素30 > MIN = 22,输出30并更改MIN = 30

13.加入3,(2、10、3)选择最小元素3 < MIN = 30,标记2为不可输出,此时,输出缓冲区中的三个元素都是不可输出元素,则第一个归并区到上一个输出元素为止(4、6、7、9、11、13、14、16、22、30)

归并段2: 

14.(2、10、3)选择最小元素2,输出2并更改MIN = 2

15.加入19,(19、10、3)选择最小元素3 > MIN = 2,输出3并更改MIN = 3

16.加入20,(19、10、20)选择最小元素10 > MIN = 3,输出10并更改MIN = 10

17.加入17,(19、17、20)选择最小元素17 > MIN = 10,输出17并更改MIN = 17

18.加入1,(19、1、20)选择最小元素1 < MIN = 17,标记1为不可输出,选择第二小的元素19 > MIN = 17,输出19并更改MIN = 19

19.加入23,(23、1、20)选择最小元素20 > MIN = 19,输出20并更改MIN = 20

20.加入5,(23、1、5)选择最小元素5 < MIN = 20,标记5为不可输出,选择第三小的元素23 > MIN = 23,输出23并更改MIN = 23

21.加入36,(36、1、5)选择最小元素36 > MIN = 36,输出36并更改MIN = 36

22.加入22,(12、1、5)选择最小元素12 < MIN = 36,标记12为不可输出时,输出缓冲区中的三个元素都是不可输出元素,则第二个归并区到上一个输出元素为止(2、3、10、17、19、20、23、36)

第三个归并段:

23.(12、1、5)选择最小元素1,输出1并更改MIN = 1

24.加入18,(12、18、5)选择最小元素5 > MIN = 1,输出5并更改MIN = 5

25.加入21,(12、18、21)选择最小元素12 > MIN = 5,输出12并更改MIN = 12

26.加入39,此时,待排序文件空,将内存工作区中的剩余数据按序输出,即18、21、39,则第三个归并段为(1、5、12、18、21、39)


最佳归并树

 

 

1.性质和构造完全相同于哈弗曼树

2.与哈弗曼树的区别:

k叉树,其中k > 2时:需要判断是否能满足构造完全k叉树,若不满足,则需要添加长度为0的“虚段”

①若(初始归并段数量 - 1) % (k - 1) = 0,则能构成完全k叉树

②若(初始归并段数量 - 1) % (k - 1)= u ≠ 0,则说明需要添加(k - 1)- u 个虚段才能构成完全二叉树
      假设我们现在要进行8路归并,初始段的数量等于19,18%7=4,7-4=3。

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卿云阁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值