866数据结构笔记 - 第八章 排序

湖大计学考研系列文章目录


目录

重点内容

一、基本概念

二、插入排序

1. 直接插入排序

2. 折半插入排序

3. 希尔排序

三、交换排序

1. 冒泡排序

2. 快速排序

四、选择排序

1. 简单选择排序

2. 堆排序

五、归并排序

六、基数排序 

七、各种内部排序的比较

八、代码题

参考(具体细节见原文)


重点内容

        22年866真题:1个选择+1个简答+1个计算+1个代码,共计32分

  1. 基于比较的算法,至少需要比较次数?
  2. 直接插入排序思想和每一步结果
  3. 希尔排序每一步结果
  4. 冒泡排序每一步结果
  5. 快速排序每一步结果及代码
  6. 简单选择排序每一步结果
  7. 堆排序每一步结果及代码
  8. 如何插入和删除堆的元素?
  9. 归并排序的每一步结果及代码
  10. 基数排序的每一步结果
  11. 内部排序的比较(时间复杂度、空间复杂度、稳定)
  12. 如何将奇数偶数(正数负数)分开?
  13. 输出第K大的元素
  14. 如何在链表上进行各种排序?

一、基本概念

  • 排序:重新排列表中的元素,使表中元素满足按关键字有序的过程。
  • 算法的稳定性:关键字相同的元素在使用某一排序算法之后相对位置不变,则称这个排序算法是稳定的,否则称其为不稳定的。稳定的排序算法不一定比不稳定的排序算法要好。
  • 排序算法的评价指标:时间复杂度、空间复杂度、稳定性。
  • 排序算法的分类:
    • 内部排序: 排序期间元素都在内存中——关注如何使时间、空间复杂度更低。
    • 外部排序: 排序期间元素无法全部同时存在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少。
  • 基于比较的算法,至少需要 \lceil log_{2}^{n!}\rceil 次比较。(期末题考过)

二、插入排序

1. 直接插入排序

  • 算法思想:每次将一个待排序的记录按其关键字大小,插入到前面已经排好序的子序列中,直到全部记录插入完成。
  • 时间复杂度:最好情况 O(n),最差情况O(n^2),平均情况O(n^2)
    空间复杂度:O(1)
    算法稳定性:稳定。

    适用性:适用于顺序存储和链式存储的线性表。
  • 代码实现(不带哨兵、带哨兵、链式(要注意一下链式实现)
// 不带哨兵,对A[]数组中共n个元素进行插入排序
void InsertSort(int A[],int n){
    int i,j,temp;
    for(i=1; i<n; i++){
        if(A[i]<A[i-1]){    	//如果A[i]关键字小于前驱
            temp=A[i];  
            for(j=i-1; j>=0 && A[j]>temp; --j)
                A[j+1]=A[j];    //所有大于temp的元素都向后挪
            A[j+1]=temp;
        }
    }
}

// 带哨兵,对A[]数组中共n个元素进行插入排序
void InsertSort(int A[], int n){
    int i,j;
    for(i=2; i<=n; i++){
        if(A[i]<A[i-1]){
            A[0]=A[i];     	//复制为哨兵,A[0]不放元素
            for(j=i-1; A[0]<A[j]; --j)
                A[j+1]=A[j];
            A[j+1]=A[0];
        }
    }
}

//对链表L进行插入排序
void InsertSort(LinkList &L){
    LNode *p=L->next, *pre;
    LNode *r=p->next;
    p->next=NULL;
    p=r;
    while(p!=NULL){
        r=p->next;
        pre=L;
        while(pre->next!=NULL && pre->next->data<p->data)
            pre=pre->next;
        p->next=pre->next;
        pre->next=p;
        p=r;
    }
}

2. 折半插入排序

  • 算法思路: 每次将一个待排序的记录按其关键字大小,使用折半查找找到前面子序列中应该插入的位置并插入,直到全部记录插入完成。
  • 注意:为了保证稳定性,当查找到和插入元素关键字一样的元素时,应该在这个元素的右半部分继续查找以确认位置。即当 A[mid] == A[0] 时,应继续在mid所指位置右边寻找插入位置。
  • 与直接插入排序相比,比较关键字的次数减少了,但是移动元素的次数没有变。时间复杂度仍为O(n^2)
//对A[]数组中共n个元素进行折半插入排序
void InsertSort(int A[], int n){ 
    int i,j,low,high,mid;
    for(i=2; i<=n; i++){
        A[0]=A[i];    		     	 //将A[i]暂存到A[0]
            low=1; high=i-1;
        while(low<=high){            //折半查找
            mid=(low+high)/2;
            if(A[mid]>A[0])
                high=mid-1;
            else
                low=mid+1;
        }
        for(j=i-1; j>high+1; --j)
            A[j+1]=A[j];
        A[high+1]=A[0];
    }
}

3. 希尔排序

  • 会写出d=5,3,1的每一步结果
  • 算法思路:先追求表中元素的部分有序,再逐渐逼近全局有序,以减小插入排序算法的时间复杂度。
  • 具体实施:先将待排序表分割成若干形如L[i,i+d,i+2d,...,i+kd] 的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到 d=1 为止。
  • 时间复杂度:希尔排序时间复杂度依赖于增量序列的函数。最差情况O(n^2)在某个特顶范围时可达O(n^{1.3})
  • 空间复杂度:O(1)
  • 算法稳定性:不稳定
// 对A[]数组共n个元素进行希尔排序
void ShellSort(ElemType A[], int n){
    int d,i,j;
    for(d=n/2; d>=1; d=d/2){  	//步长d递减
        for(i=d+1; i<=n; ++i){
            if(A[i]<A[i-d]){
                A[0]=A[i];		//A[0]做暂存单元,不是哨兵
                for(j=i-d; j>0 && A[0]<A[j]; j-=d)
                    A[j+d]=A[j];
                A[j+d]=A[0];
            }
		}
    }
}

三、交换排序

1. 冒泡排序

  • 算法思路:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i−1]>A[i]),则交换它们,直到序列比较完。如此重复最多 n-1 次冒泡就能将所有元素排好序。为保证稳定性,关键字相同的元素不交换。
  • 时间复杂度:最好情况O(n),最差情况O(n^2),平均情况O(n^2)
  • 空间复杂度:O(1)。
  • 稳定性:稳定。
  • 适用性:冒泡排序可以用于顺序表、链表。
// 交换a和b的值
void swap(int &a, int &b){
    int temp=a;
    a=b;
    b=temp;
}

// 对A[]数组共n个元素进行冒泡排序
void BubbleSort(int A[], int n){
    for(int i=0; i<n-1; i++){
        bool flag = false; 			//标识本趟冒泡是否发生交换
        for(int j=n-1; j>i; j--){
            if(A[j-1]>A[j]){
                swap(A[j-1],A[j]);
                flag=true;
            }
        }
        if(flag==false)
            return;       //若本趟遍历没有发生交换,说明已经有序
    }
}

2. 快速排序

  • 算法思路:在待排序表L[1.n]中任选一个元素pivot作为枢轴(通常取首元素),通过一趟排序将待排序表分为独立的两部分L[1..k-1]和L[k-1.n]。使得L[1.k-1中的所有元素小于pivot,L[k-1..n]中的所有元素大于等于pivot,则pivot放在了其最终位置L[k]上。重复此过程直到每部分内只有一个元素或空为止。
  • 快速排序是所有内部排序算法中性能最优的排序算法。
  • 在快速排序算法中每一趟都会将枢轴元素放到其最终位置上。(可用来判断进行了几趟快速排序)
  • 快速排序可以看作数组中n个元素组织成二叉树,每趟处理的枢轴是二叉树的根节点,递归调用的层数是二叉树的层数。
  • 时间复杂度:快速排序的时间复杂度=O(nx递归调用的层数)。最好情况О(nlogn),最差情况O(n^2),平均情况O(nlogn)。
    空间复杂度∶O(logn)
  • 稳定性:不稳定。
  • 快速排序的代码一定要会,特别是Partition
// 用第一个元素将数组A[]划分为两个部分
int Partition(int A[], int low, int high){
    int pivot = A[low];
    while(low<high){
        while(low<high && A[high]>=pivot)
            --high;
        A[low] = A[high];
        while(low<high && A[low]<=pivot) 
            ++low;
        A[high] = A[low];
    }
    A[low] = pivot;
    return low;
} 

// 对A[]数组的low到high进行快速排序
void QuickSort(int A[], int low, int high){
    if(low<high){
        int pivotpos = Partition(A, low, high);  //划分
        QuickSort(A, low, pivotpos - 1);
        QuickSort(A, pivotpos + 1, high);
    }
}

四、选择排序

1. 简单选择排序

  • 算法思路:每一趟在待排序元素中选取关键字最小的元素与待排序元素中的第一个元素交换位置。
  • 时间复杂度:无论待排序序列有序、逆序还是乱序,都需要进行 n-1 次处理,总共需要对比关键字n(n−1)/2 次,因此时间复杂度始终是O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适用性:适用于顺序存储和链式存储的线性表
// 交换a和b的值
void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

// 对A[]数组共n个元素进行选择排序
void SelectSort(int A[], int n){
    for(int i=0; i<n-1; i++){          	//一共进行n-1趟,i指向待排序序列中第一个元素
        int min = i;
        for(int j=i+1; j<n; j++){		//在A[i...n-1]中选择最小的元素
            if(A[j]<A[min])
                min = j;
        }
        if(min!=i)                     
            swap(A[i], A[min]);
    }
}
// 链表实现简单选择
void selectSort(LinkList &L){
    LNode *h=L,*p,*q,*r,*s;
    L=NULL;
    while(h!=NULL){
        p=s=h; q=r=NULL;
        while(p!=NULL){
            if(p->data>s->data){
                s=p; r=q;
            }
            q=p; p=p->next;
        }
        if(s==h)
            h=h->next;
        else
            r->next=s->next;
        s->next=L; L=s;
    }
}

2. 堆排序

堆(Heap) :可理解为一棵顺序存储的完全二叉树。

  • 大根堆:完全二叉树中,根≥左右,即L[i]≥L[2i]且L[i]≥L[2i+1]
  • 小根堆:完全二叉树中,根≤左右,即L[i]≤L[2i]且L[i]≤L[2i+1] (1<i≤[n/2])

算法思路:首先将存放在 L[1...n] 中的n个元素建成初始堆,由于堆本身的特点,堆顶元素就是最大值。将堆顶元素与堆底元素交换,这样待排序列的最大元素已经找到了排序后的位置。此时剩下的元素已不满足大根堆的性质,堆被破坏,将堆顶元素下坠使其继续保持大根堆的性质,如此重复直到堆中仅剩一个元素为止。

  • 时间复杂度:O(nlogn)。建堆时间O(n),之后进行n-1次向下调整操作,每次调整时间复杂度为O(logn)。空间复杂度:O(1)。
  • 稳定性:不稳定。
  • 堆的插入:对于大(或小)根堆,要插入的元素放到表尾,然后与父节点对比,若新元素比父节点更大(或小),则将二者互换。新元素就这样一路==“上升”==,直到无法继续上升为止。
  • 堆的删除:被删除的元素用堆底元素替换,然后让该元素不断==下坠"==,直到无法下坠为止。
// 对初始序列建立大根堆
void BuildMaxHeap(int A[], int len){
    for(int i=len/2; i>0; i--) 		//从后往前调整所有非终端结点
        HeadAdjust(A, i, len);
}

// 将以k为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len){
    A[0] = A[k];
    for(int i=2*k; i<=len; i*=2){	//沿k较大的子结点向下调整
        if(i<len && A[i]<A[i+1])	
            i++;
        if(A[0] >= A[i])
            break;
        else{
            A[k] = A[i];			//将A[i]调整至双亲结点上
            k=i;					//修改k值,以便继续向下筛选
        }
    }
    A[k] = A[0]
}

// 交换a和b的值
void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

// 对长为len的数组A[]进行堆排序
void HeapSort(int A[], int len){
    BuildMaxHeap(A, len);         	//初始建立大根堆
    for(int i=len; i>1; i--){      	//n-1趟的交换和建堆过程
        swap(A[i], A[1]);
        HeadAdjust(A,1,i-1);
    }
}

五、归并排序

  • 归并(Merge):把两个或多个已经有序的序列合并成一个新的有序表。k路归并每选出一个元素,需对比关键字k-1次。
  • 算法思想:把待排序表看作 n 个有序的长度为1的子表,然后两两合并,得到n/2⌉个长度为2或1的有序表……如此重复直到合并成一个长度为n的有序表为止。
  • 时间复杂度:每趟归并时间复杂度O(n),需要进行⌈logn⌉趟归并(将归并过程看作倒立的二叉树),所以时间复杂度为 O ( nlogn ) 
  • 空间复杂度:O(n)。
  • 稳定性:稳定。
  • n个元素进行二路归并,则总的比较次数为n×⌈logn⌉(期末题考过)
// 辅助数组B
int *B=(int *)malloc(n*sizeof(int));

// A[low,...,mid],A[mid+1,...,high]各自有序,将这两个部分归并
void Merge(int A[], int low, int mid, int high){
    int i,j,k;
    for(k=low; k<=high; k++)
        B[k]=A[k];
    for(i=low, j=mid+1, k=i; i<=mid && j<= high; k++){
        if(B[i]<=B[j])
            A[k]=B[i++];
        else
            A[k]=B[j++];
    }
    while(i<=mid)
        A[k++]=B[i++];
    while(j<=high) 
        A[k++]=B[j++];
}

// 递归操作
void MergeSort(int A[], int low, int high){
    if(low<high){
        int mid = (low+high)/2;
        MergeSort(A, low, mid);
        MergeSort(A, mid+1, high);
        Merge(A,low,mid,high);     //归并
    }
}

六、基数排序 

  • 算法思想:把整个关键字拆分为d位,按照各个关键字位递增的次序(比如:个、十、百),做d趟“分配”和“收集”,若当前处理关键字位可能取得r个值,则需要建立r个队列。
  • 分配:顺序扫描各个元素,根据当前处理的关键字位,将元素插入相应的队列。一趟分配耗时O(n)。
  • 收集:把各个队列中的结点依次出队并链接。一趟收集耗时O(r)。
  • 基数排序擅长处理的问题:
    • 数据元素的关键字可以方便地拆分为d组,且d较小。
    • 每组关键字的取值范围不大,即r较小。
    • 数据元素个数n较大。
  • 算法效率分析:
    • 时间复杂度:一共进行d趟分配收集,一趟分配需要O(n),一趟收集需要O(r),时间复杂度为O[d(n+r)],且与序列的初始状态无关.
    • 空间复杂度:O(r),其中r为辅助队列数量。
    • 稳定性:稳定。

七、各种内部排序的比较

对于每一种排序,需要明白思想,给出序列能否求出每一趟结果以及时间复杂度、空间复杂度、稳定性等。某些排序(快速排序,堆排序,归并排序)的代码要很熟悉。

八、代码题

基本可以总结为快排思想+链表实现排序

866数据结构考研真题:

  • 22年:奇数放一边,偶数放一边。
  • 15年:单链表冒泡排序。
  • 14年:输出无序序列第K大元素。
  • 05年:将负数和正数分开。
  • 04年:单链表二路归并。

湖大本科期末题:

  • 左边大于Ki,右边小于Ki。
  • 单链表直接插入排序。
  • 单链表简单选择排序。
  • 奇数放一边,偶数放一边。
  • 单链表二路归并。

1. 已知线性表按顺序存储,且每个元素都是不相同的整数型元素,设计把所有奇数移动到所有偶数前边的算法(要求时间最少,辅助空间最少)

/*
本题可采用基于快速排序的划分思想来设计算法,只需遍历一次即可,
其时间复杂度为O(n),空间复杂度为O(1)。
假设表为L[1..n],基本思想是:先从前向后找到一个偶数元素L(i),再从后向前找到一个奇数元素L(j),
将二者交换;重复上述过程直到i大于。
*/

void move(int A[],int len){
	//对表A按奇偶进行一趟划分
	int i=0,j=len-1;//i表示左端偶数元素的下标;j表示右端奇数元素的下标
	while(i<j){
		while(i<j && A[i]%2!=0) i++;//从前向后找到一个偶数元素
		while(i<j && A[j]%2!=1)	j--;//从后向前找到一个奇数元素
		if(i<j){
			Swap(A[i],A[j]);//交换这两个元素.
			i++;
            j--;
        }
    }
}

2. 试重新编写考点精析中的快速排序的划分算法,使之每次选取的枢轴值都是随机地从当前子表中选择的

/*
这类题目比较简单,为方便起见,可直接先随机地求出枢轴的下标,
然后将枢轴值与A[low]交换,而后的思想就与前面的划分算法一样。
*/

int Partition2(int A[],int low,int high){
	int rand_Index=low+rand() %(high-low+1);
	Swap(A[rand Index],A[low]);//将枢轴值交换到第一个元素
	int pivot=A[low];			//置当前表中的第一个元素为枢轴值
	int i=low;					//使得表A[low…i]中的所有元素小于pivot,初始为空表
for(int j=low+1;j<=high;j++)//从第毫个元素开始寻找小于基准的元素
	if(A[j]<pivot)				//找到后,交换到前面
		swap(A[++i],A[j]);
	swap(A[i],A[low]);			//将基准元素插入到最终位置
	return i;					//返回基准元素的位置
}

3. 试编写一个算法,使之能够在数组L[1..n]中找出第 k 小的元素(即从小到大排序后处于第k个位置的元素

/*
本题最直接的做法是用排序算法对数组先进行从小到大的排序,
然后直接提取L(k)便得到了第k小元素,但其平均时间复杂度将达O(nlog2n)以上。
此外,还可采用小顶堆的方法,每次堆顶元素都是最小值元素,时间复杂度为O(n+klog2n)。
下面介绍一个更精彩的算法,它基于快速排序的划分操作。
*/
int kth_elem(int a[],int low,int high,ing k)
{
    int pivot = a[low];
    int low temp=low;//由于下面会修改low与high,在递归时又要用到它们
    int high_temp=high;
    while(low<high){
    	while(low<high && a[high]>=pivot)
    		--high;
        a[low] =a[high];
        while(low<high && a[low]<=pivot)
            ++low;
        a[high]=a[low];
    }
    a[low]=pivot;
    //上面即为快速排序中的划分算法
    //以下就是本算法思想中所述的内容
    if(low==k)			//由于与k相同,直接返回pivot元素
    	return a[low];
    else if(low>k)		//在前一部分表中递归寻找
    	return kth_elem(a,low_temp,low-1,k);
    else				//在后一部分表中递归寻找
    	return kth_elem(a,low+1,high_temp,k-low);
}

4. 编写一个算法,在基于单链表表示的待排序关键字序列上进行简单选择排序

/*
算法的思想是:每趟在原始链表中摘下关键字最大的结点,把它插入到结果链表的最前端。
由于在原始链表中摘下的关键字越来越小,在结果链表前端插入的关键字也越来越小,
因此最后形成的结果链表中的结点将按关键字非递减的顺序有序链接。
假设单链表不带表头结点。
*/

void selectSort(LinkedList& L) {
	//对不带表头结点的单链表工执行简单选择排序
	LinkNode *h=L,*p,*q,*r,*s;
	L=NULL;
	while(h!=NULL){		//持续扫描原链表
		p = s = h;
        q = r = NULL;
		//指针s和r记忆最大结点和其前驱;p为工作指针,q为其前驱
		while(p !=NULL){	//扫描原链表寻找最大结点s
		if(p->data>s->data){ 
            s = p; 
            r = q;
        }					//找到更大的,记忆它和它的前驱
		q = p;
        p = p->1ink; 		//继续寻找
	}	
	if(s==h)
		h = h->1ink; 		//最大结点在原链表前端
	else
		r->1ink = s->1ink;	//最大结点在原链表表内
	s->1ink=L;
    L=s;					//结点s插入到结果链前端
    }
}

5.编写双向冒泡排序算法,在正反两个方向交替进行扫描,即第一趟把关键字最大的元素放在序列的最后面,第二趟把关键字最小的元素放在序列的最前面,如此反复进行(近两年期末题考过这个代码的时间复杂度分析)

/*
这种排序方法又称双向起泡。奇数趟时,从前向后比较相邻元素的关键字,遇到逆序即交换,
直到把序列中关键字最大的元素移动到序列尾部偶数趟时,从后往前比较相邻元素的关键字,
遇到逆序即交换,直到把序列中关键字最小的元素移动到序列前端。程序代码如下:
*/

void BubbleSort(int A,int n){
//双向起泡排序,交替进行正反两个方向的起泡过程
    int low = 0,high = n-1;
    bool flag = true;//一趟冒泡后记录元素是否交换标志
    while(low<high && flag){//循环跳出条件,当flag为false 说明已没有逆序
    	flag=false;//每趟初始置flag为false
    for(i=low;i<high;i++)//从前向后起泡
    	if(a[i]>a[i+1]){	//发生逆序
    		swap(a[i],a[i+1]);//交换
    		flag=true;	//置flag
   		 }
    high--;				//更新上界
    for(i=high;i>low;i--)//从后往前起泡
        if(a[i]<a[i-1]){	//发生逆序
            swap(a[i],a[i-1]);	//交换
            flag=true;		   //置flag
        }
    low++;				   	   //修改下界
}


参考(具体细节见原文)

参考书目:

  1. 王道:《数据结构》
  2. 湖大本科: 《数据结构与算法分析( C++版)(第三版)》Clifford A. Shaffer 著,张铭、刘晓丹等译

  3. 数据结构学习笔记(王道)_梦入_凡尘的博客-CSDN博客_王道数据结构笔记

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

前世忘语

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

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

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

打赏作者

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

抵扣说明:

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

余额充值