数据结构-内部排序

概述

排序的功能是将一个数据元素的任意序列,重排成一个按关键字有序的序列

  1. 对于一个序列 R 1 , R 2 , … … R n {R_1, R_2, …… R_n} R1,R2,Rn 如果 K i K_i Ki是主关键字,那么任意一个记录的无序序列经排序后得到的结果是唯一的,若 K i K_i Ki是此关键字,则排序的结果是不唯一的,因为待排序的记录序列仲可能存在两个或者两个以上的记录。

  2. 假设 K i = K j K_i=K_j Ki=Kj且在排序前的序列中 R i 领 先 于 R j R_i领先于R_j RiRj 那么如果排序后序列 R i R_i Ri仍然领先于 R j R_j Rj 则称所用的排序方法是稳定的,否则称所用的排序方法是不稳定的。

  3. 内部排序指待排序记录存放在计算机的随机存储器当中,外部排序指待排序记录数量大,在排序过程中仍需对外存进行访问的排序过程

  4. 内部排序可大致分为插入排序,交换排序,选择排序,归并排序,计数排序

插入排序

直接插入排序

将一个记录插入到已排好序的有序表中,从而得到一个新的,记录数加一的有序表。
一般情况下,直接插入排序的操作为,在含有 i − 1 i-1 i1个记录的有序子序列 r [ 1 … i − 1 ] r[1…i-1] r[1i1] 中插入一个记录 r [ i ] r[i] r[i] 后,变成有 i i i个记录的有序子序列 r [ 1 … i ] r[1…i] r[1i]

  1. 整个排序过程可以看作n-1次插入,即将序列中的第一个元素看作有序的子序列,再从第二个记录开始依次插入记录。
void InsertSort(std::vector<int> &vec)
{
	for (int i = 1; i < vec.size(); i++)
	{
		auto temp = vec[i];
		auto j = i - 1;
		while (j >= 0 && vec[j] > temp)
		{
			vec[j + 1] = vec[j];
			j--;
		}
		vec[j + 1] = temp;
	}
}
  1. 直接插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2) 因为如果序列有序,则关键字比较次数仅为 n − 1 n-1 n1 如果序列逆序,此时比较次数达最大值 ( n + 2 ) ( n − 1 ) / 2 (n+2)(n-1)/2 (n+2)(n1)/2 移动次数也为最大值。 因此操作数的期望可取这两种情况的平均值 n 2 4 \frac{n^2}{4} 4n2
折半插入排序

因为是在有序序列中查找插入位置,故查找过程可用折半查找替代,

void BInsert(std::vector<int> &vec)
{
	for(int i=1;i<vec.size();++i)
	{
		auto temp=vec[i];
		int low=0;
		int high=i-1;
		while(low<=high)
		{
			int mid = low+(high-low)/2;
			if(temp<vec[mid])
				high=mid-1;
			else
				low=mid+1;
		}
		for(int j=i-1;j>=high;j--)
			vec[j+1]=vec[j];
		vec[low]=temp;
	}
}

折半插入排序仅仅减少了关键字比较次数,时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)

2-路插入排序
  1. 另设置一个和L同类型的数组d ,并将 L [ 1 ] L[1] L[1]赋值给 d [ 1 ] d[1] d[1]并将其看作是在排好序的序列中处于中间位置的记录,然后在L中从第二个记录起依次插入到 d [ 1 ] d[1] d[1]之前或者之后的序列中。 为了便于实现,可将d实现为循环的双端队列,并记录头尾位置

  2. 在2-路插入排序中,移动记录的次数约为 n 2 8 \frac{n^2}{8} 8n2 并且如果L[1]是序列中最大或者最小的元素,那么这种插入排序方式就完全失去了优越性,退化为折半插入排序

表插入排序
  1. 可以不移动记录的排序方式,首先将数组中下标为1的分量和表头结点构成一个循环链表,再依次将下标从2到n的分量按关键字有序插入到循环链表中,在链表中插入元素无需移动元素位置只需改变指针,但是仍然需要比较元素,时间复杂度也为 O ( n 2 ) O(n^2) O(n2)
  2. 表插入排序结果是有序链表,此时有序表仍然是乱序的,为了让有序表也能折半查找,需要根据指针域对记录进行重新排列
void Arrange(SLinkList &SL)//next域只存储相应的结点下标,而非地址
{
	auto p = SL[0].next;
	for(int i=0;i<SL.Length;i++)
	{
		while(p<i)
			p=SL[p].next;
		auto q = SL[p].next;
		if(p!=i)
		{
			swap(SL[p],SL[i]);
			SL[i].next=p;
		}
		p=q;
	}
}

希尔排序

  1. 先将整个待排序记录序列分割为若干子序列,分别进行直接插入排序,待整个序列中记录基本有序时,再对全体记录进行一次直接插入排序。
  2. 子序列的构成不是逐段分割,而是将相隔某个增量的记录组成一个子序列
void Shell(vector<int> &vec, int dk)
{
	for(int i=dk;i<vec.size();i+=dk)
	{	    
        int temp=vec[i];
        int j=i-dk;
        while(j>=0&&vec[j]>temp){
            vec[j+dk]=vec[j];
            j-=dk;
        }
        vec[j+dk]=temp;
	}
}
void ShellSort(std::vector<int> &vec,std::vector<int> &inc)
{
	for(int i=0;i<inc.size();i++)
	{
		Shell(vec,inc[i]);
	}
}
  1. 该排序的时间复杂度是关于增量序列的函数,但有人指出增量序列为 2 t − k + 1 − 1 2^{t-k+1}-1 2tk+11时,排序的时间复杂度为 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23)
  2. 必须使增量序列的值没有除1之外的公因子,且最后一个增量值必须为1 (对应最后一次排序)

交换排序

冒泡排序

  1. 将第一个记录关键字和第二个记录关键字比较,若为逆序就交换两个关键字,直到第n-1个记录和第n个记录比较完成,此为第一趟冒泡排序,然后对前n-1个元素进行相同的操作,一般来说,第 i i i 次冒泡排序是从第一个元素到第n-i+1个元素。
  2. 当一趟排序过程中没有进行交换操作,说明排序完成
int main()
{
	std::vector<int> elem = { 49,38,65,97,76,13,27,49 };
	for (int i = elem.size(); i >= 1; i--)
	{
		bool flag = false;
		for (int j = 0; j < i-1; j++)
		{
			if (elem[j] > elem[j + 1])
				{
					swap(elem[j],elem[j+1]);
					flag=true;	
				}
		}
		if (!flag)
			break;
	}
}

快速排序

  1. 通过一趟排序将待排记录分割为独立的两部分,一部分的关键字均比另一部分的关键字小,则可分别对这两部分继续排序。
  2. 先选一个记录作为枢轴/支点 ,比它小的放在前,比它大的放在后面,就可以将枢轴所在位置作为分界线
  3. 步骤为从high开始向前搜索到第一个关键字小于pivotkey的记录和枢轴记录交换,再从low开始向后搜索到第一个关键字大于pivotkey的记录和枢轴记录交换,重复这两步直到high==low为止
  4. 在排序过程中对枢轴记录的赋值是多余的,因为只有在排序结束时low的位置才是枢轴的最后位置,所以应当先暂存枢轴记录到temp,最后再替换。
int Partition(vector<int> &L, int low, int high)
{
	auto temp=L[low];
	auto pivotkey=L[low];
	while(low<high)
	{
		while(low<high&&L[high]>=pivotkey)
			--high;
		L[low]=L[high];
		while(low<high&&L[low]<=pivotkey)
			++low;
		L[high]=L[low];
	}
	//此时high=low
	L[low]=temp;//枢轴的最后位置
	return low;
}
void QSort(vector<int> &L,int low,int high)
{	
	if(low<high)
	{
		auto loc = Partition(L,low,high);
		QSort(L,low,loc-1);
		QSort(L,loc+1,high);
	}
}
  1. 快速排序的平均时间为 k n ln ⁡ n kn\ln_n knlnn n为待排序序列中记录的个数,k为某个常数,就平均时间而言,快速排序是最好的内部排序方法。但如果初始序列按关键字逆序,则会退化为冒泡排序,时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,为改进之,通常采取L[s].key L[t].key L[(s+t)/2].key的中值记录为枢轴 记录
  2. 时间上看,快排优于前面讨论的其他排序方法,但是从空间上看,快排需要一个栈空间来实现递归,如果分成的两个记录序列长度相近,则栈的最大深度为 l o g 2 n + 1 log_2n+1 log2n+1 而如果不均匀分布且都偏向子序列的一端,则深度为 n n n
  3. 如果改写算法,在一趟排序后比较分割所得的两部分长度,先对短的子序列进行快排,则栈的最大深度可下降为 O ( l o g n ) O(log_n) O(logn)

选择排序

基本思想是每一趟在n-i+1个记录中选取最值作为有序序列中第i个元素

简单选择排序

在第i趟时从前n-i+1个元素中选取关键字最小的记录和第i个记录交换位置

int minimum(vector<int> &vec,int idx){
    long min=INT_MAX;
    int res=0;
    for(int i=idx;i<vec.size();i++){
        if(vec[i]<min)
        {
            res=i;
            min=vec[i];
        }
    }
    return res;
}
void Select(vector<int> &vec)
{
	for(int i=0;i<vec.size();++i)
	{
		auto j=minimum(vec,i);//j为序列中最小元素的下标
		if(i!=j)
			std::swap(vec[i],vec[j]);
	}
}
  1. 可以看出,在n个关键字中选出最小值,至少进行n-1次比较,时间复杂度O(n^2)但是继续比较剩余的关键字,不一定进行n-2次比较,可以利用前n-1次比较的信息(锦标赛排序)

树形选择排序(锦标赛排序)

  1. 首先对n个记录的关键字进行两两比较,然后在其中n/2个小者之间再进行两两比较,直到选出最小关键字的记录为止。过程可用一棵完全二叉树表示
  2. 在叶子结点中存放排序之前的关键字,而在非终端结点中存放左右孩子结点中较小的关键字,则树根结点中的关键字就是叶子结点中的最小关键字。
  3. 在输出最小关键字之后,只需将相应叶子结点关键字改为无穷大,然后再从这个叶子结点开始,修改非终端结点的关键字,则树根结点中关键字就是次小关键字。
  4. 因为完全二叉树的深度为 l o g 2 n + 1 log_2n+1 log2n+1 因此每选择一个次小关键字仅需进行 l o g 2 n log_2n log2n次比较,所以时间复杂度为 O ( n l o g n ) O(nlog_n) O(nlogn)
  5. 但是存在辅助空间较多的缺点,故提出了堆排序

堆排序

只需要一个记录大小的辅助空间,每个待排序的记录仅占有一个存储空间

  1. 堆的定义: k i < = k 2 i   & &   k i < = k 2 i + 1 k_i<=k_{2i}\space \& \&\space k_i<=k_{2i+1} ki<=k2i && ki<=k2i+1 如果将此序列对应的一维数组看成是一棵完全二叉树,则堆的含义表明所有完全二叉树中非终端结点的值均不大于/小于 左右孩子结点的值
  2. 因此如果一个序列为堆,则堆顶元素必定为序列中最大/最小值
  3. 堆排序的过程就是输出堆顶元素,再将剩余的n-1个元素的序列重建为堆,反复执行便可得到有序的序列

堆的重建/调整:
调整:输出堆顶元素之后,以堆中最后一个元素代替堆顶,此时左右子树均为堆,从上到下进行调整,即比较根结点的值和左右孩子的值,并与小的交换,直到叶子结点为止
建堆:从一个无序序列建堆的过程就是反复筛选的过程,从n/2个元素开始向前筛选,实际上就是进行调整。

void HeapAdj(vector<int> &vec,int parent,int end)
{
    auto parentNode = vec[parent];
    for(int i=2*parent+1;i<=end;i=2*i+1)//从左孩子开始
    {
        if(i<end&&vec[i]<vec[i+1])
            i++;
        if(parentNode>=vec[i])//不用调整
            break;
        vec[parent]=vec[i];
        parent=i;
    }
    vec[parent]=parentNode;
}
vector<int> HeapSort(vector<int>& vec) {
    for(int i=vec.size()/2;i>=0;i--)
        HeapAdj(vec,i,vec.size()-1);
    for(int i=vec.size()-1;i>0;i--)
    {
        swap(vec[0],vec[i]);
        HeapAdj(vec,0,i-1);
    }
    return vec;
}

堆排序的时间耗费在建初始堆和调整堆时所进行的反复筛选上,但是堆排序在最坏的情况下,时间复杂度也为 O ( n l o g n ) O(nlog_n) O(nlogn),也只需一个记录空间作为交换用的辅助存储空间

优先队列

std::priority_queue(int,vector<int>,greater<int>());
  1. 可解决K个最大值这种问题,用堆实现,

归并排序

将两个或者两个以上的有序表组合成一个新的有序表,假设初始序列有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并得到n/2个长度为2或者1的有序子序列,直到得到长度为n的有序序列为止
核心操作是将数组前后相邻两个有序序列归并为一个有序序列

std::vector<int> Merge(std::vector<int> &left,std::vector<int> &right)
{
    std::vector<int> res(left.size()+right.size(),0);
    int i=0,j=0,idxres=0;
    while(i<left.size()||j<right.size())
    {
        if(i==left.size())
            res[idxres++]=right[j++];
        else if(j==right.size())
            res[idxres++]=left[i++];
        else{
            if(left[i]<right[j])
                res[idxres++]=left[i++];
            else{
                res[idxres++]=right[j++];
            }
        }
    }
    return res;
}
std::vector<int> MergeSort(std::vector<int> &List)
{
    if(List.size()==1)
        return List;
    else{
        std::vector<int> left(List.begin(),List.begin()+List.size()/2);
        std::vector<int> right(List.begin()+List.size()/2,List.end());
        auto U=MergeSort(left);
        auto V=MergeSort(right);
        return Merge(U,V);
    }
}

归并排序需要进行 l o g 2 n 趟 log_2n趟 log2n 所以时间复杂度为 O ( n l o g n ) O(nlog_n) O(nlogn)

与快排和堆排相比(同样时间复杂度) 归并排序是一种稳定的排序方法

基数排序

  1. 借助多关键字排序的思想对单逻辑关键字进行排序。借助分配和收集两种操作对单逻辑关键字进行排序的一种内部排序方法。处理量大,关键字取值范围有限的序列,时间复杂度最低,稳定.
  2. 是桶排序的扩展,桶排序只在关键字数量比取值范围大很多使使用,每一个桶相当于一个链表,遍历时遍历每一个桶中的元素即可,而基数排序,强调对多个关键字进行排序,先进行一次桶排序,得到半有序的序列后,再用另一个关键字进行桶排序。

多关键字排序

假设有n个记录的序列 R 1 , R 2 , … … R n {R_1, R_2, …… R_n} R1,R2,Rn且每个记录 R i R_i Ri中含有d个关键字 ( K i 0 , K i 1 , K i d − 1 ) (K_i^0, K_i^1, K_i^{d-1}) (Ki0,Ki1,Kid1) 其中 K 0 K^0 K0 为最主位关键字, K d − 1 K^{d-1} Kd1为最次位关键字。

  1. 一种方法是先对最主位关键字 K 0 K^0 K0排序,进而将序列分成子序列,每个子序列记录有相同的 K 0 K^0 K0 值,然后再在每个子序列中对次位关键字排序, 最后得到的每一子序列中有相同的关键字 ( K 0 , K 1 , K d − 1 ) (K^0,K^1, K^{d-1}) (K0K1,Kd1) 最后将子序列连接起来,这种方法称为最高位优先MSD法
  2. 第二种方法是从最次位关键字开始对整个序列排序 称为最低位优先LSD法
    MSD和LSD只约定按什么关键字次序排序, 未规定每个关键字排序时使用的方法,但是MSD时将序列逐层分割为子序列,而LSD是整个序列参加排序,这就引发了一个问题,LSD在排序的时候必须采用稳定的排序方法

链式基数排序

有的逻辑关键字可以看作是多个关键字复合而成的,比如0~999 可以每一位看作一个关键字,从低位关键字开始,将序列中记录分配到“关键字取值范围”数量相同的队列中,然后按顺序收集,重复多次,就实现了基数排序,其中"基”指的是关键字取值范围RAD。

  1. 传统的基数排序,队列占用的空闲空间很大,因此我们用链表代替队列,对于n个待排数字,我们建立RAD个链表,第一趟分配,将记录按最后一位数字将其分配到不同的链表中去(链表插入元素),第一趟收集就是将RAD链表改变所有非空队列的队尾指针,重新连接为一个待排序列,第二趟收集时,按倒数第二位数字分配到不同链表,因为此时最后一位关键字有序,所以按顺序分配,链表中的记录按最后一位数字也有序。
  2. 不断重复分配-收集过程,即排序完毕,如果n个元素,每个元素含d个关键字,每个关键字取值范围为rd,则时间复杂度为 O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd)) ,每一趟分配的时间复杂度为 O ( n ) O(n) O(n) 每一趟收集的时间复杂度为 O ( r d ) O(rd) O(rd)
#include <iostream>
#include <vector>
#include <string>
#define N 6
#define RAD 27//a~z
class LinkNode
{   
public:
    LinkNode()=default;
    LinkNode(std::string _str):word(_str),next(nullptr){}
    std::string word;
    LinkNode *next;
};
void PreProcess(std::vector<std::string>& seq)//填充长度
{
    for(auto &i:seq)
    {
        while(i.size()<N)
            i.push_back(' ');
    }
}
void PostProcess(std::vector<std::string>& seq)//删除长度
{
    for(auto &i:seq)
    {
        while(i.back()==' ')
            i.pop_back();
    }
}
void Distribute(std::vector<std::string>& seq,std::vector<LinkNode>& List,int j)//按关键字的第j个分量进行分配,
{   int ind=0;
    for (int i=0;i<seq.size();i++)        
    {
        if (seq[i][j]==' ')
            ind=0;
        else
            ind=seq[i][j]-'a'+1;
        auto p=new LinkNode(seq[i]);
        LinkNode* temp=List[ind].next;
        if(temp==nullptr)
            List[ind].next=p;
        else
        {
            while(temp->next!=nullptr)
                temp=temp->next;
            temp->next=p;
        }
    }
}
void Collect(std::vector<std::string>& seq,std::vector<LinkNode>& List)//依次将各非空队列中的记录收集起来
{   
    int ind=0;
    for(int i=0;i<RAD;i++)
    {   auto temp=List[i].next;
        if(temp!=nullptr)
        {   
            while(temp!=nullptr)
            {   auto p =temp;
                seq[ind++]=temp->word;
                temp=temp->next;
                free(p);
            }
        }
    }
    for(int i=0;i<List.size();i++)
    {
        List[i].next=nullptr;
    }
}
void RadixSort(std::vector<std::string>& seq) 
{   PreProcess(seq);
    std::vector<LinkNode> vec(27);
    for (int i=N-1;i>=0;i--)            
    {
        Distribute(seq,vec,i);       
        Collect(seq,vec);                    
    }
    PostProcess(seq);

}
int main()
{
    std::vector<std::string> str={"a","brown","fox","jumped","over","the","blue","cot","cat"};
    RadixSort(str);
    for(auto &i:str)
        std::cout<<i<<" ";
}

内部排序方法讨论

方法时间复杂度最坏情况额外存储
简单 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
快排 O ( n l o g n ) O(nlog_n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( l o g n ) O(log_n) O(logn)
堆排序 O ( n l o g n ) O(nlog_n) O(nlogn) O ( n l o g n ) O(nlog_n) O(nlogn) O ( 1 ) O(1) O(1)
归并排序 O ( n l o g n ) O(nlog_n) O(nlogn) O ( n l o g n ) O(nlog_n) O(nlogn) O ( n ) O(n) O(n)
基数排序 O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd)) O ( d ( n + r d ) ) O(d(n+rd)) O(d(n+rd)) O ( r d ) O(rd) O(rd)
  1. 时间上,快排最佳,但最坏情况下不如堆排序和归并排序。n较大时,归并时间较堆排序少,但额外空间多

  2. 简单排序包括除希尔排序之外的所有插入排序,还有冒泡排序。当基本有序或者n较小,直接插入排序较为简单

  3. 基数排序适用于n很大而关键字数量较小的序列

  4. 基数排序和简单排序法、归并排序都是稳定的,而快排 堆排和希尔排序都是不稳定的

  5. 如果每个记录很大, 不方便移动记录,可以采用静态链表作为存储结构,移动游标来代替移动元素。但对于快排和堆排无法用静态链表, 可以以元素的地址代替元素本身来作为关键字,最后根据地址向量重排元素即可

  6. 借助于“比较”进行排序的算法在最坏情况下所能达到的最好时间复杂度为 O ( n l o g n ) O(nlog_n) O(nlogn)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值