排序(二)

(一) 堆排序:由于优先队列可以花费O(NlogN)时间的排序,基于该想法的排序称之为堆排序。建立N个元素的二叉堆,基本上花费的时间为O(N),再执行N次删除操作。按照顺序呢,最小的元素先离开该堆。通过将这些元素记录到第二个数组然后在将数组拷贝回来,就可得到N个元素的排序。由于每个Delete操作花费时间。

该算法的主要问题在于它使用了一个附加的数组,因此,存储需求增加了一倍,注意,将第二个数组拷贝回第一个数组的额外时间消耗只是O(N),这不可能显著影响运行时间问题,这只是一个空间问题。

避免使用第二个数组的聪明的做法是:在每次delete之后,堆缩小了1,因此,位于堆中最后的单元可以用来存放刚刚删去的元素,使用这种策略,在最后一次delete之后,该数组将以递减的顺序包含这些元素。若果想使这些元素排成典型的递增的顺序,我们可以改变序的特性,是父节点的元素大于孩子节点的元素。

在实现的过程中,我们使用一个大顶堆(MaxHeap),但是由于速度原因避免了实际上的ADT(每一件事都是在一个数组中试实现的)。第一步,一线性时间建立一个堆。然后通过将堆中的最后元素与第一个元素进行交换,缩减堆的大小,并进行下滤,来实行 N - 1 次delete操作。

堆排序是一种非常稳定的算法,它平均使用的比较次数比最坏情形下的略少。

//4. 堆排序(1)【只是用一个数组,而不必建立一个堆的数据结构(使用大顶堆):从小到大排列的】
#define LChild(i) (i * 2 + 1)  //去 i 节点的左孩子(因为cichu)

//建立一个大顶堆
void BuildMaxHeap(int A[], int i, int N)
{
 int Child;
 int tmp;  //临时变量,用于存放父节点 i 的值
 for(tmp = A[i]; LChild(i) < N; i = Child)  //i 为父节点,LChild为其孩子节点
 {
  Child = LChild(i);       //下滤过程
  if(Child != N-1 && A[Child] < A[Child + 1])  //父节点 i 与其左右孩子进行比较取最大者
   Child++;
  if(tmp < A[Child])
   A[i] = A[Child];  //使用大的孩子节点替代父节点
  else
   break;
 }
 A[i] = tmp;    //填补空穴
}

//(2)建立一个小顶堆.最后得到的结果是从大到小排列的
void BuildMinHeap(int A[], int i, int N)
{
 int Child;
 int tmp;
 for(tmp = A[i]; LChild(i) < N; i = Child)
 {
  Child = LChild(i);
  if(Child != N -1 && A[Child] > A[Child + 1])  //寻找 i 节点的最小孩子,并将其替换 i 处的元素
   Child++;
  if(tmp > A[Child])  //若父节点 i 大于最小孩子,则使用它的最小孩子替换 i 处的元素
   A[i] = A[Child];
  else
   break;
 }
 A[i] = tmp;   //填补空穴
}

void Swap(int *a, int *b)
{
 int tmp;
 tmp = *a;
 *a = *b;
 *b = tmp;
}

void HeapSort(int A[], int N)
{
 int i;
 for(i = N / 2; i >= 0; i--) //注意 i 的起始位置
 {
  //BuildMaxHeap(A, i, N);
  BuildMinHeap(A, i, N);
 }
 for(i = N - 1; i > 0; i--)                                                    
 {
  Swap(&A[0], &A[i]);  //使用一个数组
  //BuildMaxHeap(A, 0, i);
  BuildMinHeap(A, 0, i);
 }
}

(二) 归并排序:归并排序以O(NlogN)最坏情形的时间运行,而且它使用的比较次数几乎是最优的,是递归算法的一个很好的实例。

这个算法的基本操作是合并两个已经排好序的表,由于这两个表已经排好序,所以若将输出放到第三个表中时,则该算法可以同过对这两个表的一次排序来完成。基本的排序算法是取两个排序数组 A,B,一个输出数组 C,以及三个计数器 Aptr,Bptr,Cptr分别指向三个数组的首元素。A[Aptr] 和 B[Bptr]中的较小的元素被存放到C[Cptr]中,相关计数器向下移动一个位置,当一个表用完的时候,将另一个表的剩余元素拷贝到C 数组中。

合并两个已经排好序的表所用的时间显然是线性的,因为进行了N - 1次比较,其中N是数组元素的个数。归并算法很容易描述,当N = 1时,只有一个元素需要排序,结果是显然的。否则,递归的将前半部分和后半部分的数据,各自进行归并排序。得到排序后的两部分数据,然后使用上述提到的合并算法对两个一排好序的表进行合并。

    该算法是经典的分治策略:它将问题分成一些小的问题,然后递归的求解,而治的阶段是将分的阶段所得到的各个答案进行修补到一起。分治策略是递归非常有力的用法。

//归并排序(分治策略):递归
//参数:Lpos:左半部分数组的第一个元素,Rpos是右半部分数组的第一个元素,Rend是右半部分数组的最后一个元素

void Merge(int A[], int TmpArray[], int Lpos, int Rpos, int Rend)
{
 int i, Lend, NumElements, TmpPos;  //初始化

 Lend = Rpos -1;   // Lend是左半部分数组的最后一个元素
 NumElements = Rend - Lpos + 1;  //数组的总长度
 TmpPos = Lpos;    //TmpArray是一个临时数组,用于存放已经排好序的元素
 while(Lpos <= Lend && Rpos <= Rend)
 {
  if(A[Lpos] <= A[Rpos])
   TmpArray[TmpPos++] = A[Lpos++];
  else
   TmpArray[TmpPos++] = A[Rpos++];
 }
 while(Lpos <= Lend)    //左半部分数组有剩余
  TmpArray[TmpPos++] = A[Lpos++];
 while(Rpos <= Rend)    //右半部分数组有剩余
  TmpArray[TmpPos++] = A[Rpos++];

 for(i = 0; i < NumElements; i++, Rend--)  //将临时数组中的元素存放到原数组中
  A[Rend] = TmpArray[Rend];
}

void MSort(int A[], int TmpArray[], int Left, int Right)
{
 int Center;
 if(Left < Right)
 {
  Center = (Left + Right) / 2; //分界点
  MSort(A, TmpArray, Left, Center);   //分(左)
  MSort(A, TmpArray, Center + 1, Right);
  Merge(A, TmpArray, Left, Center + 1, Right); //合
 }

}

void MergeSort(int A[], int N)
{
 int *TmpArray;  // 创建一个临时数组
 TmpArray = new int[N];
 if(TmpArray != NULL)
 {
  MSort(A, TmpArray, 0, N - 1);
  free(TmpArray);
 }
 else
  throw exception("No space for tmp array!");
}

虽然归并排序的时间复杂度为O(NlogN),但它很难用于主存排序,主要问题在于合并两个排序表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组,再从临时数组中拷贝回来这样一些附加工作,其结果严重放慢的排序的速度。这种拷贝可以通过在递归交替层次审慎的交换 A 和 TmpArray 的角色得到避免。对于重要的内部排序而言人们还是选择快速排序。

 //各种排序算法
#include<iostream>
using namespace std;
typedef int ElemType;
//4. 堆排序(1)【只是用一个数组,而不必建立一个堆的数据结构(使用大顶堆):从小到大排列的】
#define LChild(i) (i * 2 + 1)  //去 i 节点的左孩子(因为cichu)
void BuildMaxHeap(int A[], int i, int N)
{
 int Child;
 int tmp;  //临时变量,用于存放父节点 i 的值
 for(tmp = A[i]; LChild(i) < N; i = Child)  //i 为父节点,LChild为其孩子节点
 {
  Child = LChild(i);       //下滤过程
  if(Child != N-1 && A[Child] < A[Child + 1])  //父节点 i 与其左右孩子进行比较取最大者
   Child++;
  if(tmp < A[Child])
   A[i] = A[Child];  //使用大的孩子节点替代父节点
  else
   break;
 }
 A[i] = tmp;    //填补空穴
}
//(2)建立一个小顶堆.最后得到的结果是从大到小排列的
void BuildMinHeap(int A[], int i, int N)
{
 int Child;
 int tmp;
 for(tmp = A[i]; LChild(i) < N; i = Child)
 {
  Child = LChild(i);
  if(Child != N -1 && A[Child] > A[Child + 1])  //寻找 i 节点的最小孩子,并将其替换 i 处的元素
   Child++;
  if(tmp > A[Child])  //若父节点 i 大于最小孩子,则使用它的最小孩子替换 i 处的元素
   A[i] = A[Child];
  else 
   break;
 }
 A[i] = tmp;   //填补空穴
}
void Swap(int *a, int *b)
{
 int tmp;
 tmp = *a;
 *a = *b;
 *b = tmp;
}
void HeapSort(int A[], int N)
{
 int i;
 for(i = N / 2; i >= 0; i--) //注意 i 的起始位置
 {
  //BuildMaxHeap(A, i, N);
  BuildMinHeap(A, i, N);
 }
 for(i = N - 1; i > 0; i--)                                                     
 {
  Swap(&A[0], &A[i]);  //使用一个数组
  //BuildMaxHeap(A, 0, i);
  BuildMinHeap(A, 0, i);
 }
}
//归并排序(分治策略):递归
//参数:Lpos:左半部分数组的第一个元素,Rpos是右半部分数组的第一个元素,Rend是右半部分数组的最后一个元素
void Merge(int A[], int TmpArray[], int Lpos, int Rpos, int Rend)
{
 int i, Lend, NumElements, TmpPos;  //初始化
 Lend = Rpos -1;   // Lend是左半部分数组的最后一个元素
 NumElements = Rend - Lpos + 1;  //数组的总长度
 TmpPos = Lpos;    //TmpArray是一个临时数组,用于存放已经排好序的元素
 while(Lpos <= Lend && Rpos <= Rend)
 {
  if(A[Lpos] <= A[Rpos])
   TmpArray[TmpPos++] = A[Lpos++];
  else
   TmpArray[TmpPos++] = A[Rpos++];
 }
 while(Lpos <= Lend)    //左半部分数组有剩余
  TmpArray[TmpPos++] = A[Lpos++];
 while(Rpos <= Rend)    //右半部分数组有剩余
  TmpArray[TmpPos++] = A[Rpos++];
 for(i = 0; i < NumElements; i++, Rend--)  //将临时数组中的元素存放到原数组中
  A[Rend] = TmpArray[Rend];
}
void MSort(int A[], int TmpArray[], int Left, int Right)
{
 int Center;
 if(Left < Right)
 {
  Center = (Left + Right) / 2; //分界点
  MSort(A, TmpArray, Left, Center);   //分(左)
  MSort(A, TmpArray, Center + 1, Right);
  Merge(A, TmpArray, Left, Center + 1, Right); //合
 }
}
void MergeSort(int A[], int N)
{
 int *TmpArray;  // 创建一个临时数组
 TmpArray = new int[N];
 if(TmpArray != NULL)
 {
  MSort(A, TmpArray, 0, N - 1);
  free(TmpArray);
 }
 else
  throw exception("No space for tmp array!");
}int main()
{
 int arr[10] = {5, 2, 8, 6, 3, 1, 7, 9, 4, 10};
 HeapSort(arr, 10);
 
 for(int i = 0; i < 10; i++)
  cout << arr[i] << " ";
 cout << endl;
 system("pause");
 return 0;
}

 

时间: 2024-11-05 16:29:33

排序(二)的相关文章

hiho一下 第四十八周 拓扑排序&#183;二【拓扑排序的应用 + 静态数组 + 拓扑排序算法的时间优化】

题目1 : 拓扑排序·二 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho所在学校的校园网被黑客入侵并投放了病毒.这事在校内BBS上立刻引起了大家的讨论,当然小Hi和小Ho也参与到了其中.从大家各自了解的情况中,小Hi和小Ho整理得到了以下的信息: 校园网主干是由N个节点(编号1..N)组成,这些节点之间有一些单向的网路连接.若存在一条网路连接(u,v)链接了节点u和节点v,则节点u可以向节点v发送信息,但是节点v不能通过该链接向节点u发送信息. 在刚

SDUT 3399 数据结构实验之排序二:交换排序

数据结构实验之排序二:交换排序 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Problem Description 冒泡排序和快速排序都是基于"交换"进行的排序方法,你的任务是对题目给定的N个(长整型范围内的)整数从小到大排序,输出用冒泡和快排对这N个数排序分别需要进行的数据交换次数. Input 连续多组输入数据,每组数据第一行给出正整数N(N ≤ 10^5),随后给出N个整数,数字间以空格分隔. Output

排序(二)__冒泡排序、简单选择排序和直接插入排序

前面<排序(一)__综述>提到按照算法的复杂度分为简单算法和改进算法两大类,本文主要就简单算法中的冒泡排序.简单选择排序和直接插入排序进行通俗详细的解析. 一.冒泡排序 1.基本概念 冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止.(动态图来自维基百科) 2.关键代码(优化之后) void BubbleSort(SqList *L) { int i,j; Status flag=TRUE;            //flag用作标记,避

(转载)排序二 快速排序

排序二 快速排序 目录 要点 算法分析 快速排序算法的性能 时间复杂度 空间复杂度 算法稳定性 完整参考代码 JAVA版本 参考资料 相关阅读 要点 快速排序是一种交换排序. 快速排序由C. A. R. Hoare在1962年提出. 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数. 然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列. 详细的图解往往比大堆的文字更有说明力,所以直接上图:

hihoCoder - 1175 - 拓扑排序&#183;二 (拓扑排序的应用)

#1175 : 拓扑排序·二 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho所在学校的校园网被黑客入侵并投放了病毒.这事在校内BBS上立刻引起了大家的讨论,当然小Hi和小Ho也参与到了其中.从大家各自了解的情况中,小Hi和小Ho整理得到了以下的信息: 校园网主干是由N个节点(编号1..N)组成,这些节点之间有一些单向的网路连接.若存在一条网路连接(u,v)链接了节点u和节点v,则节点u可以向节点v发送信息,但是节点v不能通过该链接向节点u发送信息.

SDUT-3399_数据结构实验之排序二:交换排序

数据结构实验之排序二:交换排序 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 冒泡排序和快速排序都是基于"交换"进行的排序方法,你的任务是对题目给定的N个(长整型范围内的)整数从小到大排序,输出用冒泡和快排对这N个数排序分别需要进行的数据交换次数. Input 连续多组输入数据,每组数据第一行给出正整数N(N ≤ 10^5),随后给出N个整数,数字间以空格分隔. Output 输出数据占一行,代表冒泡排序

算法之排序二

算法之排序二 四.冒泡排序与插入排序       为何在实际中倾向于使用插入排序而不是冒泡排序,尽管它们的时间复杂度都是O(n2),而且也都是稳定的.看一下两个算法在交换元素数值的处理上就知道了.对于冒泡排序,交换两个元素时需要引入中间变量,也就是如果需要交换 A 和 B,我们需要让 A 赋值给 C,然后让 A 等于 B,再让 B 等于 C.而插入排序在每次比较时会把大的元素往后移,要插入的时候直接插入,所以更加的直接,在实际应用时更常用.在 Python 上测试一下也可以知道,冒泡排序比插入排

二维数组里,根据数组字段为条件,进行总体排序(二维排序)

1 <?php 2 /** 3 * 二维数组根据某个字段排序 4 * 功能:按照用户的年龄倒序排序 5 * @author ruxing.li 6 */ 7 header('Content-Type:text/html;Charset=utf-8'); 8 $arrUsers = array( 9 array( 10 'id' => 1, 11 'name' => '张三', 12 'age' => 25, 13 ), 14 array( 15 'id' => 2, 16 '

排序二 快速排序

要点 快速排序是一种交换排序. 快速排序由C. A. R. Hoare在1962年提出. 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数. 然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列. 详细的图解往往比大堆的文字更有说明力,所以直接上图: 图-快速排序示例图 图-快速排序示例图中,演示了快速排序的处理过程: 初始状态为一组无序的数组:2.4.5.1.3. 经过以上操作步骤后,完成

数据结构和算法之排序二:快速排序

上一篇文章我们讲完了归并排序,对于分而治之和递归思想应该都有了一定的理解,这篇文章我们将介绍道被认为是排序算法中最容易出错,但是又是最喜欢使用的一中排序方式,快速排序.对于快速排序而言我们必须抓住几个关键点就是基准值的选取,以及它在递归思想的运用过程中需要注意的事项.我们先看下面的图片了解一下快速排序的过程. 我们可以看出每一次排序过程中都是选取第一个数据作为基准值,然后在前段和末端设置两个指针,指针对应的数据和基准值进行比较,如果大于基准值我们将它放在右边,如果小于基准值,我们就将它放在左边.