排序算法总结

概述

复杂度计算
复杂度计算

冒泡排序 Bubble Sort

有长度为n的数组
数据相邻比较,一轮循环后结果极值在最后,因此再进行n-1次循环,每次完成极值位置排序。
第一次从[0] 到 [n-1],第二次从[1]到[n-2]…因此有

for(int i=0;i<length;i++){
	//j从1开始防止越界
	for(int j=1;j<length-i;j++){
		//进行比较
		swap(str[j], &str[j-1]);
	}
}

稳定性
相对位置不会变化,属于稳定的排序。

优化

  • 原本的数列已经完成排序,可以提前终止
    数组有序,则表示每次比较都没有交换数据,也就是IF的条件没有成立,因此可以进行标记

如何标记?
排序完毕有两种情况:一种是已经完全排序,另一种是进行某次循环后有序,因此,在每次第一层for循环进行一次标记,即假定每次开始循环前有序,若进行比较则无序。每次内层for循环结束后进行判断。

for(int i=0;i<length;i++){
	//j从1开始防止越界
	bool =truefor(int j=1;j<length-i;j++){
		//进行比较
		swap(str[j], &str[j-1]);
		bool =false}
	if (bool) break;
}

如果不会达到提前完成排序的结果,则会多出bool判断的操作。

  • 数列尾部已经完成排序,可以记录最后一次排序的位置,减少次数
    如果数组的后半部分有序,则可以在排序的时候省略对他们的排序,减少运算

如何标记?
通过IF判断可以标记,如果IF成立,则表示进行交换,可以设置SortIndex记录交换位置,每次排序后,下次进行的循环到SortIndex为止。

如何设置标记值的初始值?
因为最后标记值设置成end值,那么如果数列完全有序,则一次循环后结束,因此标记值可以设置成1,以达到停止排序的结果。

for(end = array.length; end > 1;){
		SortIndex =1; //每次内层循环前设置
		for( begin=1;begin<end;begin++){
			//进行判断 
			if(array[begin]>array[begin-1]){
			temp=array[begin];
			array[begin]=array[begin-1];
			array[begin-1]=temp;	
			SortIndex=begin;
			}
		}
		//下次进行循环则到end为止。
		end = SortIndex;
	}

注意
使用标记值时,可以去去除for循环内end–的操作,初始Index设置为1,逻辑上表示若不排序则直接停止循环。

时间复杂度为o(N²)
空间复杂度为o(1)

选择排序 Selection Sort

从[0]到[n-1]依次选择数据,与剩下的每个数据进行比较,若满足判断条件则交换。

for(int i=0;i<length-1;i++){
		for(int j=i+1;j<length;j++){
			if(str[i]>str[j]){
				temp=str[i];
				str[i]=str[j];
				str[j]=temp;
			}
		}
}

时间复杂度为o(N²)
空间复杂度为o(1)

稳定性
例如[5,8,5,2,9],第一次交换,第一个5和2交换,两个5的相对位置变化,属于不稳定的排序。

堆排序 Heap Sort

堆排序是对于选择排序的优化:将每次选择最值的操作交给堆操作,可以提高效率

步骤
1.原地建堆
2.重复下述操作,直到堆的元素为1:
a. 进行移项(交换堆顶和堆尾操作),堆Size-1;
b. 对0号位置进行siftdown操作

1.建堆:
a.先建立完全二叉树
b.从最后一个非叶节点进行调整:
与左右结点比较:若左右节点大,则与之交换,并考察该节点为根的子树是否满足要求,不满足则递归处理。

int heap_size;
//交换
void swap(int *a,int *b){
	int temp = *a;
	*a = *b;
	*b = temp;
	return;
}
//返回左子树索引
int left(int index){
	return ((index<<1)+1);
}
//返回右子树索引
int right(int index){
	return ((index<<1)+2);
}
//建立堆
void build_heap(int array[],int length){
	heap_size = length;
	//非叶节点的遍历
	for(int i = ((heap_size-1)>>1);i>=0;i--){
		max_heap_adjust(array,i);
	}	
}
//每个非叶节点的处理
void max_heap_adjust(int array[],int index){
	int largest=index;  //设置初始最大节点为根节点
	int right_index = right(index);
	int left_index = left(index);
	//进行比较(限制节点不能超过length)
	if(left_index < heap_size && array[left_index] > array[largest]){
		largest=left_index;
	}
	if(right_index < heap_size && array[right_index] > array[largest]){
		largest=right_index;
	}
	//判断是否交换
	if(largest == index){
		return;
	}
	else{
		swap(array[index], array[largest]);
		//进行递归,判断交换后的子节点是否满足条件。
		max_heap_adjust(array, largest);
	}
}

2.排序

heap_sort(){
	void heap_sort(int array[] , int length){
	int old_heap_size = length;
	for(int i=heap_size;i>1;i--){
		swap( &array[0], &array[i-1]);
		//每次进行调换位置后,需要将堆元素-1
		heap_size--;
		max_heap_adjust(array,0);
	}
	heap_size = old_heap_size;
}
}

进行堆排序时间复杂度为o(nlog(n))
堆排序不是稳定的排序算法。

插入排序 Insertion Sort

插入排序类似于扑克牌整理顺序的方式:每抓到一张牌,将其放入相应位置。

冒泡插入
从下标为1开始排序,每次都和相邻的元素进行比较完成排序

void insertsort(int array[], int length){
	//从1开始  
	for(int begin = 1;begin < length;begin ++){
		int cur = begin; //记录当前begin 
		while(cur > 0 && array[cur] > array[cur-1]){
			swap( &array[cur],  &array[cur-1] );
			cur--;
		}
	}
}

该种方法的时间复杂度和数列的逆序对数量成正比,最好为o(N),最坏情况为o(N²)

挪动插入

1.备份待插入元素
2.将头部有序的数和待插元素比较,如果比该值要大(小)则均往前挪一位。之后再将待插元素插入。

void insertsort(int array[], int length){
	//从1开始  
	for(int begin=1;begin<length;begin++){
		//记录当前begin 
		int cur = begin; 
		//备份数据
		int v = array[cur];
		//如果当前值比该值大,则该值往前一位 
		while(cur > 0 && v > array[cur-1]){
			array[cur]=array[cur-1];
			cur--;
		}
		array[cur] = v;
	}
}

优化

二分法搜索插入
通过二分法搜索插入,减少寻找位置的次数,达到提高效率
将每次要处理的头部已经排好序的数列进行二分搜索,找到应该插入的位置并返回。

int binarySearch(int array[],int index){
	int begin = 0;
	int end = index;	
	while(begin < end){
		int mid = (begin + end) >> 1;
		if(array [mid] > array[index]){
			end = mid;
		}
		else{
			begin = mid + 1;
		}
	}
	return begin;
} 

void insertsort(int array[], int length){ 
	for(int begin = 1;begin < length; begin++){
		int v = array[begin];
		int insertindex = binarySearch(array,begin);
		//将 [insertindex , begin ]内的元素往后挪一位 
	    for(int i = begin ; i > insertindex; i--){
	    	array[i]=array[i-1]; 
		}
		//将待插入元素插入 
		array[insertindex] = v;
	}
}

优化后的时间复杂度依然是o(N²)(减少的仅仅是寻找的次数,挪动的次数依旧没有变化)

归并排序 Merge Sort

1.Divide 将数组不断分成2个子序列,直到每个序列只剩下一个元素
2.Merge 将分好的子序列不断合并

Divide

每次分开,需要进行递归调用,不断的拆分直到子序列只有一个元素。

void MergeSort(int array[],int length,int begin,int end){
	//如果只有一个元素 
	if(end - begin < 2) return;
	
	//进行divide 
	int mid = (begin + end) >> 1;
	MergeSort(array, mid-begin, begin, mid);
	MergeSort(array, end-mid, mid, end);
	Merge(array, begin, mid, end);	
}

Merge

为了将分开的2个序列进行排序,并且完成的序列在原数组中,为了保证尽量少用空间,我们只需要为了左数组再额外建立空间进行排序。

如何合并
对每个数组设置索引:
左数组 Li = 0 ; Le = mid; (左数组在新空间内)
右数组 Ri = mid ; Re = length = end (右数组在原数组内)
原数组 Ai = 0
由于子序列已经排好序,因此每次比较索引点位置数据即可。

特殊情况
若左数组已经全部插入,由于右数组已经在原数组中,因此排序可以提前结束。
若右数组已经全部插入,则将左数组剩余的内容直接插入原数组。

void Merge(int array[],int begin, int mid, int end){
	int li = 0, le = mid - begin;
	int ri = mid, re = end;
	int ai = begin;
	
	//备份左数组:
	for(int i = li; i < le; i++)
		leftarray[i] = array[begin + i];	
	//根据左数组进行判断
	while(li < le){
		if(ri < re && leftarray[li] > array[ri] ){
			array[ai++] = array[ri++];
		}
		else{
			array[ai++] = leftarray[li++];
		}		
	} 
}

归并排序属于稳定的排序,时间复杂度为O(n log n)

快速排序 Quick Sort

1.从序列中选择一个轴点元素pivot(一般选择0位置元素)
2.利用pivot分割序列(比pivot大的放右边,小的放左边)
3.对子序列不断重复 1,2操作,直到子序列只有一个元素

本质:逐渐对每个元素转换成轴点元素

确定轴点
开始时,对轴点元素进行备份,从end位置开始从右往左扫描:
若end>pivot ,则不需要挪动,标记值end–;
若end<=pivot,则将array [end] 覆盖begin位置(pivot所在位置),同时begin++,从begin处开始从左往右扫描。
当begin = end 时,将pivot覆盖array [begin] 值,构造结束。

如何设置“掉头”?
可以设置2个while循环,在循环内执行到else语句时(即需要转移值时)break

递归调用
确定轴点后,对左右两个子序列分别再次确定轴点,直到子序列无法拆分。

void Quicksort(int array[], int begin, int end){
	if(end -begin <2) return;
	
	int pivot = PivotIndex(array,begin,end);
	
	//对轴点两边的2个子序列进行快速排序 
	Quicksort(array,begin,pivot);
	Quicksort(array,pivot+1,end);
}

//返回轴点位置,同时对序列进行轴点化 
int PivotIndex(int array[], int begin, int end){
	//备份轴点值
	int v = array[begin]; 
	//使用的是开区间,比较要从end-1开始 
	end--;
	while(begin < end){
		
		//2个while控制方向 
		while(begin < end){
			if( v < array[end])     //右边元素>轴点元素 
				end--;
			else{
				array[begin++] = array [end];
				break;
			}
		}
		
		while(begin < end){
			if( v > array[begin])  //左边元素<轴点元素 
				begin++;
			else{
				array[end--] = array[begin];
				break; 
			}
		}
	} 
	array[begin] = v;
	return begin;
}

快速排序属于不稳定的排序。

时间复杂度
当轴点左右元素均匀时,能快速得到结果:
T(n) = 2* T(n/2) +O(n) = O(n log n)
当轴点左右元素极度不均匀,则最坏结果:
T(n) = T(n-1) +O(n) = O(n²)
为了避免出现该情况,可以选择随机的初始轴点元素:
可以在开始选取时,随机选择元素和begin元素进行交换。

希尔排序 Shell Sort

将序列看成矩阵,分为m列,生成一个步长序列,由此步长序列逐渐将m减为1.
每次排序后,逆序对数量逐渐减少,因此希尔排序的底层使用插入排序
希尔给出的序列为N/2的k次。例如20,对应的步数序列为{10,5,2,1};

生成希尔步长序列
使用list结构进行存储。

list <int> StepSequence;
list <int> ::iterator it ;

void ShellStepSequence(int length){
	int step = length;
	while( (step >>= 1)>0){
		StepSequence.push_back(step);
	}	
}

固定使用希尔序列进行排序(底层使用最简单的插入排序)

void Shellsort(int array[]){
	int step =array.length;
	while((step = step>>1)>0){
		//第几列 
		for(int col = 0; col <step; col++){
			//对 col col+step col+2*step 进行插入排序
			for(int begin = col+step; begin< length; begin += step){
				int cur =begin;
				while (cur > col && array[cur] < array[cur -step] ){
					swap( array[cur], array[cur-step]);
					cur -= step;
				}
			} 
		}
	}
}

时间复杂度
使用希尔的步长序列,最坏情况为O(n²)
当前最好的序列,最坏情况是O(n的4/3次)
在这里插入图片描述
最好情况的序列生成

计数排序 Counting Sort

计数排序不是比较排序,而是用于一定范围内的整数,用空间换时间的做法进行排序

核心
统计每个整数在序列中出现的次数,并推导每个整数在有序序列中的索引。
索引代表的值为该数出现的次数。
创造一个临时计数空间,空间取决于数组的最大值。

void Countingsort(int array[], int length){
	//找到最大值
	int max = array[0];
	for(int i=0;i<length;i++){
		if(array[i] > max)
			max = array[i];
	} 
	
	//开辟内存空间并初始化,存储每个整数出现的次数
	int *counts = new int [1 + max]; 
	for(int i=0; i<1+max; i++){
		counts[i]=0;
	} 
	
	//统计每个数出现的次数
	for(int i=0; i<length; i++){
		counts[array[i]]++;
	} 
	
	//将count内数组输出至array
	int index =0 ;  //设置array的下标 
	for(int i=0; i<max+1; i++){
		//根据counts[i]的数据决定赋值几次 
		while(counts[i]-- >0){
			array[index++] = i; 
		}
	} 
	delete counts;
}

该类型的计数排序,时间复杂度为O(N),但是会有如下缺点:
1.无法对负整数实现
2.只求最大值极度浪费空间
3.不是一个稳定的排序

优化

针对上述问题进行改进:
1.由于直接找到数据对应索引,会导致无法排序负整数。因此可以将索引结构进行调整。
2.可以找到最小值和最大值,长度由他们决定(max-min+1)。同时还可以改变原数据索引结构
如原来的元素 k,对应索引 i=k-min 。
3.索引对应数据counts[i]对应的次数,改为每个次数累加上前面的次数,这样代表该元素的位置信息。

如何建立count数组?
1.确定大小为 max-min+1.
2.确定每个元素对应次数(count[i])
3.对每个次数进行累加计算。(累加产生结果为该元素的位置信息。)

如何利用counts排序?
改进后,对原数组array[] 从右到左进行遍历 (这样可以产生稳定的排序)
拿到一个元素,找到对应元素的索引值 count[i], 然后 count[i]-=1,得到在array[]数组中存放的位置。

void Countingsort(int array[],int length,int result[]){
	
	//找到最值
	int min = array[0];
	int max = array[0];
	for(int i = 0;i < length;i++){
		if(array[i]>max)
			max = array[i];
		else if(array[i]<min)
			min = array[i];
	}  
	
	//创建counts数组
	int *counts = new int[max -min+1];
	for(int i=0;i<max-min+1;i++)
		counts[i] = 0;
	
	//添加数据,得到元素位置信息
	for(int i =0; i<length;i++){
		//得到元素出现次数 
		counts[array[i] -min] ++;
	} 
	for(int i =1; i<max-min+1;i++){
		//累加得到位置信息 
		counts[i] +=counts[i-1];
	}
	
	
	//对array数组从右往左遍历 ,结果存入result
	for(int i =length-1;i>=0;i--){
		result[--counts[array[i] -min] ]=array[i];
	} 
		
}

最好、最坏、平均时间复杂度为O(N+k);
k为整数取值范围;
属于稳定的排序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值