数据结构笔记|排序|C语言

203 阅读13分钟
  • 关键字的取值情况,会使排序结果可能不唯一。
  • 按照主关键字排序,排序结果唯一;按照次关键字排序,排序结果可能不唯一。
  • 若对任意的数据元素序列,使用某个排序方法,对它按关键字进行排序:若相同关键字元素间的位置关系,排序前与排序后保持一致,称此排序方法是稳定的;而不能保持一致的排序方法则称为不稳定的。
typedef struct {
	int r[Max];
	int length;
}SqList;
SqList L;

一、插入排序

(一)直接插入排序

1716826521239.png 思路讲解:【数据结构】算法题 直接插入排序_哔哩哔哩_bilibili

//待排序的数据元素从下标为1的数组元素开始存放。下标0处为哨兵项
void insertSort(SqList L) {
	int i, j;
	for(i = 2; i <= L.length; i++)//从第二个数据开始插入,n = L.length
		if(L.r[i].key < L.r[i-1].key) {//第i个数据比前面已经有序的i-1个数据最大的小
			L.r[0] = L.r[i];//将第i个数据放入哨兵位置
			L[i] = L.r[i-1];
			for(j = i-2; L.r[0].key < L.r[j].key; --j)///L.r[0]存放的是此次要插入的第i个数据
				L.r[j+1] = L.r[j];
				L.r[j+1] = L.r[0];
		}
}
  • 直接插入排序是一个稳定的排序方法
  • 平均时间复杂度为:O(n2),
  • 一个额外的辅助空间, O(1) 。
  • 对于有n个数据元素的待排序序列,插入操作要进行n-1趟。
  • 最好情况: n-1次数据比较,0次数据移动。待排序的数据基本有序,时间复杂度O(n)
  • 最坏情况:待排序的数据逆序。 i 比较次数 移动次数 2 2 3 3 3 4 …… n n n+1
  • 数据比较次数=(n+2)(n-1)/2
  • 数据移动次数=(n+4)(n-1)/2
  • 直接插入排序,待排序的数据用数组和链表存放均可
  • 可证平均时间复杂度约为:n^2/4。当n<16时, n^/4<nlogn。理论值当n<16时,直接插入比O(nlogn)的排序方法快!

(二)二分插入排序

1716827922269.png

void BinsertSort(SqList &L) {
	int i, low, high, mid;
	for(i = 2; i <= L.length; i++)
		if(L.r[i].key < L.r[i-1].key) {
			L.r[0] = L.r[i];
			low = 1;
			high = i-1;
			while(low <= high) {
				mid = (low +high)/2;
				if(L.r[0].key < L.r[mid].key)high = mid - 1;
				else low = mid +1; 
			}
			for(j = i-1; k >= high + 1; j--)
				L.r[j+1] = L.r[j];
			L.r[high + 1] = L.r[0];
		}
} 
  • 二分插入排序减少了关键字的比较次数,但数据元素的移动次数不变,其时间复杂度与直接插入排序相同。
  • 时间复杂度:O(n^2)
  • 待排序的数据元素必须存放于数组

(三)希尔排序

1716859097080.png 思路:插入排序——希尔排序_看不懂你打我系列_哔哩哔哩_bilibili
性能分析:待排序数据:7,7,4
我们表示为:7(1) ,7(2) ,4
第一趟(d1=2):4,7(2) ,7(1)
第二趟(d2=1):4,7(2) ,7(1)
关键字值相同的数据元素7在排序前后的相对位置发生了变化,所以希尔排序是不稳定的排序方法

二、交换排序

(一)快速排序

1716860152440.png

int partition(SqList L, int l, int h) {
	L.r[0] = L.r[l];//取待排序的第一个数据元素为基准放到L.r[0]
	//原来的L.r[l]初始相当于空着 
	//从后面开始找 
	while(l < h) {//一趟快排结束的条件是左右边界重合
		//若右边界大于基准,则右边界左移一位
		while((l<h) && (L.r[h].key >= L.r[0].key)) 
			h--;
                        //右边界小于基准,则把右边的大数放到前面的空位
		if(l < h) {
			L.r[l] = L. r[h];
			l++;
		}
		//若左边界小于基准,则左边界右移一位
		while((l<h) && (L.r[h].key >= L.r[0].key)) 
			l++;
                        //若大于,则把左边的小数移到后面的空里
		if(l < h) {
			L.r[h] = L. r[l];
			h--;
		}
		//l=h跳出循环 
		L.r[l] = L.r[0];//把基准元素放到这个重合处的空里 
		return l;//返回基准的位置 
			
		
	}
} 

//递归,结束的条件是待排序的数据元素个数小于等于1 
void QSort(SqList &L. int l, int h) {
	int t;
	if(l<h) { //待排序的数据有2个或2个以上才进行排序操作
		t = partition(L, l, h);//调用一趟快排,t为返回的基准的位置
		QSort(L, l, t-1);//对比基准小的子序列继续进行快速排序
		QSort(L, t+1, h);//对比基准大的子序列继续进行快速排序
}
} 

稳定性:不稳定
例:待排序的数据:7,4,4----》7,4(1),4(2)
第一趟快排:4(2),4(1) , 7
第二趟快排:4(2),4(1) , 7
关键字值相同的数据元素4在排序前后的相对位置发生了变化
平均时间复杂度为O(nlog(2,n))
最坏情况的时间复杂度为O(n^2)
最坏情况待排序的记录基本有序:正序(递增)、逆序(递减)
正序:比较次数总计n(n-1)/2

(二)冒泡排序

  • 将待排序的数据元素的关键字顺次两两比较,若为逆序(前大后小)则将两个数据元素交换
  • 将序列照此方法从头到尾处理一遍称作一趟冒泡排序,它将关键字值最大的数据元素交换到排序的最终位置
  • 对n个元素排序最多需要n-1趟冒泡排序
  • 结束条件:做完n-1趟,或:某趟冒泡排序过程中一次数据移动也没发生,说所有数据已经有序,冒泡排序结束
  • 最好情况:n个数据元素,1趟冒泡排序,0次数据移动,n-1次比较(初始的待排序序列恰好是递增有序,R1≤R2 ≤…… ≤Rn)
  • 最坏情况: n个数据元素, n-1趟冒泡排序。第1趟比较n-1次,移动3(n-1)次,……总计n-1趟,比较n(n-1)/2次,移动3n(n-1)/2次(初始的待排序序列恰好是逆序 ,R1≥R2 ≥ …… ≥ Rn)
  • 平均时间复杂度O(n^2)
  • 一个额外的辅助空间O(1)
  • 冒泡排序是稳定的排序方法

步骤:
第一趟:第1个与第2个比较,第1个大则交换;第2个与第3个比较,第2个大则交换,……,n-1次比较后,关键字最大的数据元素交换到最后一个位置上,此时最后一个元素有序
第二趟:对前n-1个数据元素进行同样的操作,n-2次比较后,关键字次大的数据元素交换到第n-1个位置上;
依次类推,则完成排序。

void qppx(SqList &L) {
	int i, j, k;
	j = 1;
	k = 1;
	//共有n-1趟排序:j从1到length-1 
	while((j < L.length) && (k > 0)) {
		k = 0;
		//第j趟要比较n-j次:i从1到n-j 
		for(i = 1; i <= L.length-j; i++)
			if(L.r[i+1].key < L.r[i].key]) {//出现逆序,利用辅助空间r[0]交换 
				L.r[0] = L.r[i];
				L.r[i] = L.r[i+1];
				L.r[i+1] = L.r[0];
				k++;//k记录每一趟排序中发生的交换次数,交换则+1 
			}
			j++;} 
	}

三、选择排序

(一)简单选择排序

步骤:
n个数据元素进行n-1趟扫描
第1趟扫描:进行n-1次比较,选出n个数据元素中关键字值最小的数据元素,与第1个数据元素交换;
第i趟扫描:进行(n-i)次比较,选出剩下的n-i+1个数据元素中关键字值最小的数据元素,并与第i个数据元素交换;

  • 时间复杂度为O(n2)
  • 最好、最坏、平均时间复杂度都是O(n^2)
  • 不论输入的待排序的数据是什么顺序(最好、最坏都一样),每一趟简单选择排序的比较次数不变,总的比较次数为:(n-1)+(n-2)+…+2+1=n(n-1)/2次
  • 最好情况:第一趟找到的最小的恰好在第一个位置,不发生数据交换,…, 每一趟找到的最小的数据都不要交换,输入的待排序的数据恰好有序,总的数据移动次数为0次。
  • 最坏情况:第一趟找到的最小的要交换到第一个位置,数据移动3次,…, 每一趟找到的最小的都要交换,数据移动3次。总共n-1趟,总的数据移动次数3(n-1)次。
  • 适用于待排序元素较少的情况
  • 不稳定
    例:待排序的数据:2,2,1----》2(1),2(2) ,1
    第一趟简单选择排序:1,2(2) ,2(1)
    第二趟简单选择排序: 1,2(2) ,2(1)
    关键字值相同的数据元素2在排序前后的相对位置发生了变化
void SelectSort(SqList &L) {
	int i, j, k;
	for(i = 1; i < L.length; i++) {//进行n-1趟排序 
		k = i;//第i趟时,初始设当前最小的数据为第k=i个数据元素
		for(j = i+1; j <= L.length; ++j)
		//从第i+1个到第n个,依次和当前最小的第k个数据元素比较,若比第k个还小,则更新k
			if(L.r[j].key < L.r[k].key)
				k = j;
			if(k != i) {
				//交换借用额外的辅助空间L.r[0], O(1) 
				L.r[0] = L.r[i];
				L.r[i] = L.r[k];
				L.r[k] = L.r[0];
			}
		} 
	}
} 

树排序 1716875285723.png 1716875398610.png

树排序将时间复杂度降为O(nlogn)但需要的辅助空间增加
步骤看ch10-3ppt,很清晰

(二)堆排序

1717033945679.png 步骤:

  1. 以大顶堆为例,对一组待排序的数据元素,将它们建成一个大顶堆(称为初建堆),关键字值最大的数据为堆序列的第一个数据。(筛选法/插入法)n个数据建堆从n/2个数据开始进行调整,即从下到上、从右至左找到第一个非叶结点开始进行调整。
  2. 将关键字值最大的数据取出(通常与尚未排序的最后一个数据交换存储位置)
  3. 用剩下的数据元素重建堆(称为一次调整),便得到关键字值次大的数据元素
  4. 如此反复,直到全部数据排好序
  • 时间复杂度为O(nlog(2,n))
  • 适用于待排序元素较多的情况
  • 一个额外的辅助空间O(1)
  • 不稳定。待排序的数据:2,2,1----》2(1),2(2) ,1
void HeapSort(SqList &L) {
	int i, j, k;
	for(i = L.Length/2; i>0; --i)//筛选法建堆,从n/2处开始调整
		HeapAdjust(L, i, L.length); //调整以i为根结点的子树为一个大顶堆
	for(i = L.Length; i>1; --i) {
		//n-1趟堆 排序,当前大顶堆中的数据元素i个,L.r[1]中是i个数据元素中的最大值
		L.r[0] = L.r[i];
		L.r[i] = L.r[1];
		L.r[1] = L.r[0];
		//将堆中最大的数据元素L.r[1]交换到第i个位置,也是它最终排序后的位置
		HeapAdjust(L, 1, i-1);
		//堆中数据元素个数为i-1,将i-1个数据元素重新调整为大顶堆
	}
} 
//函数HeapAdjust(L,i,L.Length)-调整以i为根结点的子树为一个大顶堆
//调整以s为根结点的子树为一个大顶堆 ,堆中最大的数据元素编号为m,且以s为根的子树中除根结点s外,均满足大顶堆的定义
void HeapAdjust(SqList &L, int s, int m) {
	int j;
	L.r[0] = L.r[s];
	for(j = 2*s, j <= m; j = j*2) {
		if(j < m && L.r[j].key<L.r[j+1].key) ++j;//j为左、右孩子中最大的那个
		if(L.r[0].key >= L.r[j].key) break;//
		L.r[s] = L.r[j];
		s = j;
	}
	L.r[s] = L.r[0];
}

四、归并排序

基本思想:

  • 将待排序序列划分成若干有序子序列
  • 将两个或两个以上的有序子序列 “合并” 为一个有序序列
  • 通常采用2-路归并排序,即:将两个位置相邻的有序子序列“合并” 为一个有序序列

算法分析:

  • 一趟归并的时间复杂度为 O(n)
  • 总共需进行log(2,n)趟
  • n个记录进行归并排序的时间复杂度为Ο(nlogn)
  • 稳定 子序列确定方法:
    1.自顶向下:若待排序的序列包含多于1个数据元素,则将其一分为二 1717036272755.png 2.自底向上:待排序的每个数据元素为一个子序列 1717036240326.png
void Merge(int SR[], int &TR[], int s, int m, int t) {
	// 将有序的序列 SR[s..m] 和 SR[m+1..t]归并为有序的序列 TR[s..t]
	for(i = s, j = m+1, k = s; i <= m && j <= t; ++k) {
		//两个序列的同位置元素相比,小的先放
		if(SR[i].key <= SR[j].key)
			TR[k] = SR[i++];
			// k为合并后的有序序列 TR[s..t]的存放位置,第一个位置为s
		else TR[k] = SR[j++]; 
	}
	//第一个有序序列 还有数据没有比较,将其复制到合并后的序列;
	if(i <= m)
		for(; i <= m; )
			TR[k++] = SR[i++];
	//第二个有序序列 还有数据没有比较,将其复制到合并后的序列;
	if(j <= t)
		for(; j <= t; )
			TR[k++] = SR[j++];
	}
} 

void Mergesort(SqList &L) {
	MSort(L.r, L.r, 1, L.length);
} 

void Msort(int SR[], int &TR1[], int s, int t) {
	//将SR[s..t] 归并排序为 TR1[s..t]
	if(s == t) TR1[s] = SR[s];//序列中只有一个数据元素,序列自然有序   
	else { //序列中包含2个及以上元素
		m = (s+t)/2; //计算序列的中间位置,以此为界划分为2个序列
		//将待排序的数据元素序列划分为:SR[s...(s+t)/2]和SR[(s+t)/2 + 1...t]
		
		Msort(SR, TR2, s, m);//对第一个子序列递归调用归并排序算法,使其有序
		Msort(SR, TR2, m+1, t);//对第二个子序列递归调用归并排序算法,使其有序
		Merge(TR2, TR1, s, m, t);//将2个有序子序列合并为一个有序序列	
}
}

五、基数排序

(一)桶排序

基本操作:

  • 分配关键字:根据每个数据元素关键字的值将其分配到相应的桶中
  • 每个“桶”内排序 数据元素均匀分布,则每个桶中数据元素个数均匀
  • 收集桶
  • 不能x像比较排序那样以统一的数据元素之间的“比较”次数衡量“工作量 1717043410747.png 两种排序方法:
  1. 最高位优先MSD法;
  2. 最低位优先LSD法:先对Kd-1进行分配排序,再对Kd-2 进行分配排序,……, 依次类推,直至最后对最高位关键字分配排序完成为止

通常采用低位优先——简单方便

下面介绍低位优先法

1717043624435.png

************这个代码不懂

#define MAX_NUM_OF_KEY 8//关键字个数最大值
#define radix 10//队列个数
#define MAX_SPACE 1000

//数组链表,用来存储序列 
typedef struct {
	int keys[MAX_NUM_OF_KEY];
	int next;
}SLCell;

typedef struct {
	SLCell R[MAX_SPACE];
	int keynum;//关键字个数
	int recnum;//待排序数据元素个数
}SLList;
typedef int ArrType[radix];

//分配 
void Distribute(SLCell &R,int i,ArrType &f,Arrtype &r,int head) {
	//静态链表的数据元素从数组下标为0处存放,next为-1代表是尾结点
	for(j=0;j<radix;j++) f[j]=-1;
	for(p=head;p!=-1;p=R[p].next) {
		j=ord(R[p].keys[i]);//示意性操作,取R[p]的第i个关键字
		if (f[j]==-1)f[j]=p;
		else R[r[j]].next=p;
		r[j]=p;
	}	
}

//收集 
void collect(SLCell &R, int i, ArrType f, ArrType r, int &head) {
	for(j=0; j<Radix &&f[j]==-1; j++);
	head=f[j]; t=r[j];
	while(j<Radix) {
		for (++j; j<Radix-1 && f[j]==-1; j++);
		if(f[j]!=-1) {
			R[t].next=f[j];
			t=r[j]; 
		}
}
	R[t].next=-1;
}

void RadixSort(SLList &L) {//建立静态链表,数据元素从数组下标为0处存放,
//next成员为-1代表是尾结点
//head存放链表的头指针
	for(j=0; j<L.recnum-1; j++)
		L.R[j].next=j+1;
	L.R[L.recnum-1].next=-1; 
	head=0; 
	for(i=0; i<L.keynum ; i++) { 
		Distribute (L.R,i,f,r;head);
		Collect(L.R,i,f,r,head);
	}
}

总结

8ab0ebc9c311416171292213a940aa1.png 1409052faa569bcf519e5b373ad5d64.png

8470f560bf73ae563093aa7b890582a.png 14807e0a18dcd8a8af79ba196207adb.png

堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法