数据结构——排序

229 阅读14分钟

排序:

按关键字的 非递减 或 非递增 顺序 对一组记录重新进行排列的操作

排序算法可以分为 稳定 和 不稳定的:

1.不稳定排序算法:

不稳定排序,即如果a = b 且a原本在b的前面,排序后 a 排到 b 的后面

选择排序,快速排序,希尔排序,堆排序

2.稳定排序算法:

稳定排序,即如果a = b,且 a 原本在 b 的前面,排序后 a 排到 b 的后面

冒泡排序,插入排序,归并排序,基排序

排序算法可以分为内部排序和外部排序:

由于待排序记录的数量不同,使得排序过程中数据所占用的存储设备会有所不同。根据在排序过程中记录所占用的存储设备,可以将排序分为两大类:

内部排序 :待排序记录全部存放在计算机内存中进行排序的过程

外部排序 :待排序记录的数量很大,以致于内存一次无法容纳全部记录,在排序中需要对外存进行访问的排序过程。

内部排序

内部排序的过程 是一个逐步扩大记录的有序序列长度 的过程。在排序过程中可以将排序记录区分为两个区域:有序序列区和无需序列区。

!!!使得有序区中记录的数目增加一个或增加几个的操作成为一趟排序!!!

  • 根据逐步扩大记录有序序列长度的原则不同,可以将内部排序分为以下几类:

1.插入类:将无序子序列 中的一个或几个记录 插入到有序序列中,从而增加记录的有序子序列的长度。包括 “直接插入排序” “折半插入排序” “希尔排序”

2.交换类:通过交换无序序列中的记录 从而得到其中关键字的最大 或 最小的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。主要包括 “冒泡排序” “快速排序”

3.选择类:从记录的无序子序列中 选择 关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。主要包括 “简单选择排序” “树形选择排序” “堆排序”

4.归并类:通过 归并 两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。2-路归并排序是最为常见的归并排序方法。

5.分配类:唯一一类不需要进行关键字之间比较的排序方法,排序时主要利用分配和收集两种基本操作完成。基数排序是最主要的分配类排序方法。

  • 待排序记录的存储方式

顺序表:记录之间的次序关系由其存储位置决定,实现排序需要移动记录

链表:记录之间的次序关系由指针指示,实现排序不需要移动记录,仅需修改指针即可。

1. 插入排序

(1)直接插入排序:将一条记录插入到已经排好序的有序表中,从而得到一个新的,记录数量增加1 的有序表。(稳定排序)

算法步骤:
  1. 设待排序的记录存放在数组r[1...n]中,
  2. 循环n-1 次,每次使用循序查找法,查找r [ i ] (i=2,3,...,n),在已排好序的序列 r [1...i-1] 中的插入位置,然后将 r [ i ] 插入表长为 i -1 的有序序列 r [1...i-1] ,直到将r [ n ]插入到表长为 i - 1的 有序序列 r [1...i-1] ,最后得到一个表长为n的有序序列。
将r[0]设置为监视哨(就是中间变量)
// 对顺序表 L 做直接插入排序

void InsertSort(SqList &L)
{
    int i,j;
    //因为有序序列中元素个数为1时,一定有序,所有i从第二个开始操作
    for(i=2;i<=L.length;i++)
    {
        if(L.r[i]<L.r[i-1]) //当要插入元素,已经大于前一个的时候,就不需要再比较了
        {
            L.r[0] = L.r[i];
            L.r[i] = L.r[i-1]; //r[i-1]后移至r[i]的位置
            
            for(j=i-2 ; L.r[0]<L.r[j] ; j--)//从第i-2个开始比较,比r[0]大的就后移,腾出位置
            {
                L.r[j+1] = L.r[j];
            }
            L.r[j+1] = L.r[0] // 归位
        }
    }
}

时间复杂度: O(n²)

空间复杂度: O(1)

(2)折半插入排序:直接插入排序法使用了顺序式查找,此处使用“折半查找”来是按查找,由此进行插入排序(稳定排序)

适用于初始记录无序,数目较大时

算法步骤:
  1. 设待排序的记录存放在数组 r [1...n]中,r [1]是一个有序序列
  2. 循环n-1次,每次使用折半查找法,查找r [i] (i=1,2,...,n)在已经排好序的序列r [i,...i-1]中插入位置,然后将r [i]插入表长为i-1的有序序列r [1...i-1],直到将r[n]插入表长为n-1的有序序列r[1,...,n-1],最后得到一个表长为n的有序序列。
//对顺序表L做折半插入排序

void BInsertSort (SqList &L)
{
    int i,j;
    int low=0,high=0,m=0; //查找区间和中间值
    
    for(i=2;i<=L.length;i++)
    {
        L.r[0] = L.r[i];//将待插入元素存放在监视哨中
        low = 1;high = i-1;
        
        while(low<=high) //在r[low...high]中折半查找要插入的位置
        {
            m = (low+high)/2;
            if(L.r[i]>L.r[m])
            {
                low = m+1;
            }
            else
            {
                high = m-1;
            }
            
        }
        for(j = i-1;j>=high+1;j--) //元素后移
        {
            L.r[j+1] = L.r[j]; 
        }
        L.r[high+1] = L.r[0];//归位
    }
    
}

时间复杂度 O( n² )

空间复杂度 O( 1 )

3.希尔排序(缩小增量排序):直接插入排序,当待排序记录个数较少且待排序序列的关键字基本有序时,效率较 高,希尔排序基于以上两点,从“减少记录个数”和“序列基本有序”两个方面进行了改进。(不稳定排序)

算法步骤:希尔排序的实质是 采用分组插入 的方式。先把待排序记录分割成几组,从而减少参与直接排序的数据量,再对每个分组进行直接插入排序,然后增加每组的数据量,重新分组,这样经过几次分组排序后,整个序列基本有序,再对全体记录进行一次直接插入排序

希尔排序对记录的分组,并不是简单的 “逐段分割” 而是将 相隔某个“增量”的记录 分成一组。

  1. 第一趟取增量 d_1 (d_1 < n) 把全部记录分成d_1个组,间隔为 d_1 的 两个记录 分在同一组,在各个组中进行直接插入排序。
  2. 第二趟取增量 d_2 (d_2 < d_1),重复上述的分组和排序
  3. 依此类推,直到所取的增量 d_t (d_t < d_t-1 < ... < d_2 < d_1),所有记录在同一组中进行直接插入排序为止
//arr[0]是暂存位
void shell_sort (int arr[],int n)
{
    int i,j,inc;
    for(inc = n/2 ; inc > 0 ; inc /= 2)
    {
        for(i = inc ; i < n ; i++)
        {
            arr[0] = arr[i];
            
            for(j = i;j >= inc && arr[0] < arr[j - inc];j -= inc) //腾位置,当增量inc不为1时,只有两两交换(可能是),当inc == 1 ,才能循环腾位置
            {
                arr[j] = arr[j-inc];
            }
            arr[j] = arr[0];
        }
    }
}

时间复杂度:当n趋于无穷大时,可以减少到 n(log2n)²
空间复杂度:只需要一个辅助空间arr[0],因此是 O(1)

交换排序

  • 基本思想是,两两比较待排序记录的关键字,一旦发现两个记录不满足次序要求,则进行交换,直到整个序列全部满足要求为止。

(1)冒泡排序:一种最简单的交换排序方法,两两比较相邻记录的关键字,如果发生逆序,则进行交换,从而使得关键字小的记录 左移,或者使得关键字大的记录 右移。

算法特点:

  1. 稳定排序

  2. 可用于链式存储

3.移动记录粗疏较多,算法的平均时间性能 比直接插入排序要差,当初始记录无序时、n较大时,不宜采用

算法步骤:
  1. 待排序的记录存放在数组 r [1...n]中。首先将第一个记录的关键字和第二个记录的关键字进行比较,若为逆序(即 不符合你的要求 比如说你要 从小到大 排序,但它是 从大到小 的顺序),就交换两个记录。然后比较第二个记录和第三个记录的关键字。以此类推,直至第 n-1 个记录和第 n 个记录的关键字进行过比较为止。上述过程为第一趟起泡排序,它的结果使得最大或最小的记录被安置到了最后一个记录的位置。

  2. 然后进行第二趟排序 ,对前 n-1 个记录进行操作,其结果是令第二大或第二小的记录,被安置在第 n-1 个记录的位置。

  3. 重复上述比较和交换过程,直到在某一趟中没有进行过交换记录的操作,说明序列已经全部达到排序要求。

// 递增排序
void BubbleSort (SqList &L)
{
    int m = L.length-1;
    int flag = 1; // flag 用来标记某一趟排序是否发生交换
    int temp=0; // 中间变量
    
    while((m > 0) && flag == 1)
    {
        flag = 0; //flag置为 0 ,如果本趟排序没有发生交换,下一趟排序就不会执行
        for(j=1;j<=m;j++)
        {
            if(L.r[j+1]<L.r[j])
            {
                flag = 1; // 表示本趟排序进行了交换
                temp = L.r[j+1];
                L.r[j+1] = L.r[j];
                L.r[j] = temp;
                
            }
           
        }
         m-- ;
    }
}

时间复杂度 O(n²)

空间复杂度 O(1)

(2) 快速排序:由冒泡排序改进而得。在冒泡排序中,只对相邻的两个记录进行了比较,因此每次交换两个相邻记录时,只能消除一个逆序。如果能通过 两个(不相邻)记录的一次交换,消除多个逆序,则会大大加快排序速度。快速排序方法中的一次交换可以消除多个逆序。

算法特点:

  1. 不稳定排序,因为是非顺次移动的

  2. 需要定位表的上下界,因此适用于顺序式结构

  3. 当n较大时,在平均情况下,快速排序是所有内部排序方法中,速度最快的一种,因此适用于初始记录无序、n较大时的排序

// 划分函数

int Partition (SqList &L, int low ,int high)
{ //对顺序表L中的子表r[low...high]进行一趟排序,并返回枢轴的位置

    int pivotkey; //用于保存枢轴记录关键字
    L.r[0] = L.r[low]; //用子表的第一个记录 作为枢轴记录
    pivotkey = L.r[0];
    
    while(low < high) //从表两端交替的向中间扫描
    {
        while(low < high && L.r[high] >= pivotkey) //循环找出该表从最右边开始,第一个比枢轴小的元素
        {
            high--; // 没找到的话,high指针不断左移
        }
        
        L.r[low] = L.r[high]; //比枢轴小的元素,移到低端
        
        while(low < high && L.r[low] <= pivotkey) //循环找出该表从最左边开始,第一个比枢轴大的元素
        {
            low++; //没找到的话,low指针不断右移
        }
        
        L.r[high] = L.r[low];
    }
    
    // 此时low == high ,两个指针在同一个位置,该位置就是枢轴的正确位置
    
    L.r[low] = L.r[0]; //枢轴归位 L.r[high] = L.r[0];
    
    return low; //返回枢轴的位置
    
}

// 主调函数

void QSort(SqList &L,int low,int high)
{
    int pivotloc; //存储枢轴
    
    if(low < high)
    {
        pivotloc = Partition(L,low,high);
        QSort(L,low,pivotloc - 1); //对左子表递归排序
        QSort(L,pivotloc + 1,high); //对右子表递归排序
        
    }
    
}

//调用函数

void QuickSort (SqList &L)
{
    QSort(L,1,L.length);
    
}

时间复杂度 :理论上可以证明,平均情况下,快速排序的时间复杂度为 O(n log2n).

空间复杂度 :快速排序是递归的,执行时需要一个栈来存放相应的数据,最大递归调用次数与递归树的深度一致,最好情况下为O(log2n),最坏情况为O(n).

选择排序

选择排序的基本思想是,每一趟从待排序的记录中,选出关键字最小的记录,按顺序放在已排序的记录序列的最后,直到全部排完为止。

(1)简单选择排序:也称为直接选择排序

算法步骤

  1. 设待排序的记录存放在数组 r[1...n] 中,第一趟从 r[1] 开始,通过 n-1 次比较,从 n 个记录中选出关键字最小的记录,记为 r[k] 交换 r[1] 和 r[k] 。

  2. 第二趟从 r[2] 开始,通过 n-2 次比较,从 n-1 个记录中选出关键字最小的记录,记为 r[k] ,交换 r[2] 和 r[k] 。

  3. 以此类推,第 i 趟 从 r[i] 开始,通过 n-i 次比较 从 n-i+1 个记录中选出关键字最小的记录,记为 r[k] ,交换 r[i] 和 r[k]。

  4. 经过 n-1 趟,排序完成。

算法特点

  1. 不稳定排序。

  2. 可以用于链式存储。

  3. 移动记录的次数较少,因此 当 每一记录 占用空间较多时,简单选择排序比直接插入排序快。

void SelectSort(SqList &L)
{
    int i,j,min,temp;
    
    for(i=1;i<=L.length;i++)
    {
        min = i;
        for(j=i+1;j<L.length;j++)
        {
            if(L.r[j]<L.r[min])
            {
                min = j;  
            }
        }
        if(i!=min) //找到最小值
        {
            temp = L.r[i];
            L.r[i] = L.r[min];
            L.r[min] = temp;
        }
    }
}

时间复杂度:O(n²) 最好情况是不移动,最坏情况(逆序)是移动3(n-1)次。
空间复杂度:同冒泡排序一样,只有在两个记录交换时需要一个辅助空间,空间复杂度为O(1)。

(2)堆排序:是一种树形选择排序

在排序过程中,将待排序的记录 r[1...n] 看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中,双亲结点和 孩子结点 之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录。

算法特点

  1. 不稳定排序

  2. 只能用于顺序存储,不能用于链式存储

  3. 初始建堆时,所需的比较次数较多,因此记录较少时不适合


//建堆以及重新调整堆

void HeapSort(SqList &L)
{//将待排序的序列构建为一个大顶堆

    int i;
    int temp;
    
    for(i = L.length ; i > 0 ; i--)
    {
        HeapAdjust(L,i,L.length);
    }
    for(i = L.length ; i > 1 ; i++)
    {
        // 将 堆顶记录 和 当前未经排序的子序列 的 最后一个记录 交换
        temp = L.r[1];
        L.[1] = L.r[i];
        L.r[i] = temp;
        
        HeapAdjust(L,1,i-1); //将L.r[1...i-1]重新调整为大顶堆
    }
}

//堆调整函数

void HeapAdjust(SqList &L,int s,int m) //s 是根节点的下标  m 是传进来的元素总个数
{
    int temp,j;
    temp = L.r[s]; //将根节点记录
    for(j = s * 2 ; j <= m ; j++)
    {
        if(j < m L.r[j] < L.r[j+1]) //j < m 说明它不是最后一个结点
        {
            j++;
        }
        if(temp >= L.r[j]) //根节点比左右孩子中最大的还大,那么直接结束循环
        {
            break;
        }
        L.r[s] = L.r[j]; //大的孩子 上浮到根节点的位置
        s = j; 
    }
    L.r[s] = temp
    
}
时间复杂度:最坏情况下,时间复杂度为:O(n log2 n)。
空间复杂度:O(1)。

归并排序

下面以二路归并为例

//合并
void merge(int arr[],int tempArr[],int left,int mid,int right)
{
   
    
    int l_pos = left;   //标记左半区第一个未排序的元素
    
    int r_pos = mid+1;  //标记右半区第一个未排序的元素
    
    int pos = left;   //临时数组下标
    
    //合并
    while(l_pos <= mid && r_pos <= right)
    {
        if(arr[l_pos] < arr[r_pos]) // 左半区第一个元素小
        {
            tempArr[pos++] = arr[l_pos++];
        }
        else // 右半区第一个元素小
        {
            tempArr[pos++] = arr[r_pos++];
        }
    }
    
   
    while(l_pos <= mid)  // 合并左半区剩余元素
    {
        tempArr[pos++] = arr[l_pos++];
    }
    while(r_pos <= right) // 合并右半区剩余元素
    {
        tempArr[pos++] = arr[r_pos++];
    }
    while(left<=right)
    {
        arr[left] = tempArr[left];
        left++;
    }
    
    
}
//归并排序
void msort(int arr[],int tempArr[],int left,int right)
{
    int mid;
    //如果只有一个元素就不需要划分,默认有序,直接进行归并排序
    if(left < right)
    {
        //找中间点
        mid = (left + right) / 2;
        //递归划分左区域
        msort(arr,tempArr,left,mid);
        //递归划分右区域
        msort(arr,tenpArr,mid+1,right);
        //合并排好序的区域
        merge(arr,tempArr,left,mid,right);
    }
}


//归并排序入口
void merge_sort(int arr[] , int n)
{
    //辅助数组
    int *tempArr = (int *)malloc(n*sizeof(int ));
    if( tempARR )
    {
        msort(arr,tempArr,0,n-1);
        free(tempArr);
    }
    else
    {
        cout<<"error:failed to allocate memory"<<endl;
    }
}

空间复杂度:O(N)

时间复杂度:O(NlogN)