>일반적인 문제 >8가지 정렬 알고리즘은 무엇입니까?

8가지 정렬 알고리즘은 무엇입니까?

青灯夜游
青灯夜游원래의
2021-05-10 16:23:5612499검색

8가지 주요 정렬 알고리즘: 1. 직접 삽입 정렬, 3. 단순 선택 정렬, 5. 버블 정렬, 7. 병합 정렬, . /기수 정렬.

8가지 정렬 알고리즘은 무엇입니까?

이 튜토리얼의 운영 환경: Windows 10 시스템, Dell G3 컴퓨터.

정렬에는 내부 정렬과 외부 정렬이 있습니다. 내부 정렬은 데이터 레코드가 메모리에 정렬되는 반면, 외부 정렬은 정렬된 데이터가 너무 커서 정렬된 모든 레코드를 한 번에 수용할 수 없기 때문입니다. 접근해야 합니다.

여기서 다루는 8가지 주요 정렬은 내부 정렬입니다.

​​​​​​​​

​​n이 큰 경우에는 시간 복잡도가 O(nlog2n)인 정렬 방법(퀵 정렬, 힙 정렬 또는 병합 정렬)을 사용해야 합니다.

퀵 정렬: 현재 비교 기반 내부 정렬 중 가장 좋은 방법으로 간주됩니다. 정렬할 키워드가 무작위로 분포되어 있는 경우 퀵 정렬의 평균 시간이 가장 짧습니다.

1. —직선 삽입 정렬


기본 아이디어:

정렬된 순서 목록에 레코드를 삽입하면 레코드 수가 1 증가한 새로운 순서 목록을 얻습니다. 즉, 먼저 시퀀스의 첫 번째 레코드를 정렬된 하위 시퀀스로 처리한 다음 전체 시퀀스가 ​​정렬될 때까지 두 번째 레코드를 하나씩 삽입합니다.

핵심 사항: 임시 저장을 위한 센티넬을 설정하고 배열 경계를 판단합니다.

직접 삽입 정렬의 예:

삽입된 요소와 동일한 요소가 발견되면 삽입된 요소는 삽입하려는 요소를 동일한 요소 뒤에 배치합니다. 따라서 동일한 요소의 순서는 변경되지 않았습니다. 정렬되지 않은 원래 시퀀스의 순서는 정렬 이후의 순서입니다.

알고리즘 구현:

void print(int a[], int n ,int i){
	cout<<i <<":";
	for(int j= 0; j<8; j++){
		cout<<a[j] <<" ";
	}
	cout<<endl;
}


void InsertSort(int a[], int n)
{
	for(int i= 1; i<n; i++){
		if(a[i] < a[i-1]){               //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
			int j= i-1;	
			int x = a[i];		 //复制为哨兵,即存储待排序元素
			a[i] = a[i-1];           //先后移一个元素
			while(x < a[j]){	 //查找在有序表的插入位置
				a[j+1] = a[j];
				j--;		 //元素后移
			}
			a[j+1] = x;		 //插入到正确位置
		}
		print(a,n,i);			//打印每趟排序的结果
	}
	
}

int main(){
	int a[8] = {3,1,5,7,2,4,9,6};
	InsertSort(a,8);
	print(a,8,8);
}
시간 복잡도: O(n^2).

기타 삽입 정렬에는 이진 삽입 정렬과 양방향 삽입 정렬이 포함됩니다.

2. 삽입 정렬 - Hill 정렬(Shell`s Sort)

Hill 정렬은 1959년 D.L. Shell이 ​​제안했으며 직접 정렬에 비해 크게 개선되었습니다. 힐 정렬은

증분 정렬 감소

이라고도 합니다. 기본 개념: 먼저 정렬할 레코드의 전체 시퀀스를 여러 개의 하위 시퀀스로 나누어 직접 삽입 정렬을 하고, 전체 시퀀스의 레코드가 나올 때까지 기다립니다. " "기본적으로 순서대로"로 설정하려면 모든 레코드를 직접 삽입하고 순서대로 정렬합니다.

작업 방법:

    증분 시퀀스 t1, t2,...,tk를 선택합니다. 여기서 ti>tj, tk=1입니다.
  • 증분 시퀀스 k의 수에 따라 수행합니다. 시퀀스 k-패스 정렬
  • 각 정렬 패스마다 정렬할 열은 해당 증분 ti에 따라 길이가 m인 여러 하위 시퀀스로 나뉘며 각 하위 테이블에서 직접 삽입 정렬이 수행됩니다. . 증분 인수가 1인 경우에만 전체 시퀀스를 테이블로 처리하며, 테이블의 길이는 전체 시퀀스의 길이가 된다.
  • Hill 정렬의 예:
쉘 정렬의 정렬 프로세스

정렬할 파일에 10개의 레코드가 있고 해당 키워드는 49, 38, 65, 97, 76, 13, 27, 49라고 가정합니다. , 55,04.

증분 계열의 값은 다음과 같습니다: 5, 3, 1

알고리즘 구현: 단순히 증분 시퀀스를 처리합니다. 증분 시퀀스 d = {n/2,n/4 , n/8 .....1} n은 정렬할 숫자의 개수입니다

즉: 먼저 특정 증분으로 레코드 세트를 정렬합니다. d (n/2, n은 정렬할 숫자의 개수입니다. ) 여러 개의 하위 시퀀스 그룹으로 나누고, 각 그룹에 기록된 첨자는 d만큼 다릅니다. 각 그룹의 모든 요소에 대해 직접 삽입 정렬을 수행한 다음 각 그룹에서 더 작은 증분(d/2)으로 그룹화합니다. 직접 삽입 정렬. 1이 될 때까지 계속해서 증가분을 줄이고 마지막으로 직접 삽입 정렬을 사용하여 정렬을 완료합니다.

void print(int a[], int n ,int i){
	cout<<i <<":";
	for(int j= 0; j<8; j++){
		cout<<a[j] <<" ";
	}
	cout<<endl;
}
/**
 * 直接插入排序的一般形式
 *
 * @param int dk 缩小增量,如果是直接插入排序,dk=1
 *
 */

void ShellInsertSort(int a[], int n, int dk)
{
	for(int i= dk; i<n; ++i){
		if(a[i] < a[i-dk]){			//若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
			int j = i-dk;	
			int x = a[i];			//复制为哨兵,即存储待排序元素
			a[i] = a[i-dk];			//首先后移一个元素
			while(x < a[j]){		//查找在有序表的插入位置
				a[j+dk] = a[j];
				j -= dk;			 //元素后移
			}
			a[j+dk] = x;			//插入到正确位置
		}
		print(a, n,i );
	}
	
}

/**
 * 先按增量d(n/2,n为要排序数的个数进行希尔排序
 *
 */
void shellSort(int a[], int n){

	int dk = n/2;
	while( dk >= 1  ){
		ShellInsertSort(a, n, dk);
		dk = dk/2;
	}
}
int main(){
	int a[8] = {3,1,5,7,2,4,9,6};
	//ShellInsertSort(a,8,1); //直接插入排序
	shellSort(a,8);			  //希尔插入排序
	print(a,8,8);
}

希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。希尔排序方法是一个不稳定的排序方法。

3. 选择排序—简单选择排序(Simple Selection Sort)


基本思想:

在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

简单选择排序的示例:

 

操作方法:

第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;

第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;

以此类推.....

第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,

直到整个序列按关键码有序。

算法实现:

void print(int a[], int n ,int i){
	cout<<"第"<<i+1 <<"趟 : ";
	for(int j= 0; j<8; j++){
		cout<<a[j] <<"  ";
	}
	cout<<endl;
}
/**
 * 数组的最小值
 *
 * @return int 数组的键值
 */
int SelectMinKey(int a[], int n, int i)
{
	int k = i;
	for(int j=i+1 ;j< n; ++j) {
		if(a[k] > a[j]) k = j;
	}
	return k;
}

/**
 * 选择排序
 *
 */
void selectSort(int a[], int n){
	int key, tmp;
	for(int i = 0; i< n; ++i) {
		key = SelectMinKey(a, n,i);           //选择最小的元素
		if(key != i){
			tmp = a[i];  a[i] = a[key]; a[key] = tmp; //最小元素与第i位置元素互换
		}
		print(a,  n , i);
	}
}
int main(){
	int a[8] = {3,1,5,7,2,4,9,6};
	cout<<"初始值:";
	for(int j= 0; j<8; j++){
		cout<<a[j] <<"  ";
	}
	cout<<endl<<endl;
	selectSort(a, 8);
	print(a,8,8);
}

 简单选择排序的改进——二元选择排序

简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。具体实现如下:

/** 这是伪函数, 逻辑判断不严谨
void selectSort(int r[],int n) {
	int i ,j , min ,max, tmp;
	for (i=1 ;i <= n/2;i++) {  
		// 做不超过n/2趟选择排序 
		min = i; max = i ; //分别记录最大和最小关键字记录位置
		for (j= i+1; j<= n-i; j++) {
			if (r[j] > r[max]) { 
				max = j ; continue ; 
			}  
			if (r[j]< r[min]) { 
				min = j ; 
			}   
	  }  
	  //该交换操作还可分情况讨论以提高效率
	  tmp = r[i-1]; r[i-1] = r[min]; r[min] = tmp;
	  tmp = r[n-i]; r[n-i] = r[max]; r[max] = tmp; 
 
	} 
}
 */
void selectSort(int a[],int len) {
        int i,j,min,max,tmp;  
        for(i=0; i<len/2; i++){  // 做不超过n/2趟选择排序 
            min = max = i;  
            for(j=i+1; j<=len-1-i; j++){  
				//分别记录最大和最小关键字记录位置
                if(a[j] > a[max]){  
                    max = j;  
                    continue;  
                }  
                if(a[j] < a[min]){  
                    min = j;  
                }  
            }  
			//该交换操作还可分情况讨论以提高效率
            if(min != i){//当第一个为min值,不用交换  
                tmp=a[min];  a[min]=a[i];  a[i]=tmp;  
            }  
            if(min == len-1-i && max == i)//当第一个为max值,同时最后一个为min值,不再需要下面操作  
                continue;  
            if(max == i)//当第一个为max值,则交换后min的位置为max值  
                max = min;  
            if(max != len-1-i){//当最后一个为max值,不用交换  
                tmp=a[max];  a[max]=a[len-1-i];  a[len-1-i]=tmp;  
            }
			print(a,len, i);			
        }  
 }

4. 选择排序—堆排序(Heap Sort)


堆排序是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:

堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足

时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:

(a)大顶堆序列:(96, 83,27,38,11,09)

  (b)  小顶堆序列:(12,36,24,85,47,30,53,91)

初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序

因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:

1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2)将根结点与左、右子树中较小元素的进行交换。

3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).

4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).

5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:


再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。

2)筛选从第个结点为根的子树开始,该子树成为堆。

3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)


 算法的实现:

从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。

void print(int a[], int n){
	for(int j= 0; j<n; j++){
		cout<<a[j] <<"  ";
	}
	cout<<endl;
}



/**
 * 已知H[s…m]除了H[s] 外均满足堆的定义
 * 调整H[s],使其成为大顶堆.即将对第s个结点为根的子树筛选, 
 *
 * @param H是待调整的堆数组
 * @param s是待调整的数组元素的位置
 * @param length是数组的长度
 *
 */
void HeapAdjust(int H[],int s, int length)
{
	int tmp  = H[s];
	int child = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)
    while (child < length) {
		if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)
			++child ;
		}
		if(H[s]<H[child]) {  // 如果较大的子结点大于父结点
			H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点
			s = child;		 // 重新设置s ,即待调整的下一个结点的位置
			child = 2*s+1;
		}  else {			 // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
			 break;
		}
		H[s] = tmp;			// 当前待调整的结点放到比其大的孩子结点位置上
	}
	print(H,length);
}


/**
 * 初始堆进行调整
 * 将H[0..length-1]建成堆
 * 调整完之后第一个元素是序列的最小的元素
 */
void BuildingHeap(int H[], int length)
{ 
	//最后一个有孩子的节点的位置 i=  (length -1) / 2
	for (int i = (length -1) / 2 ; i >= 0; --i)
		HeapAdjust(H,i,length);
}
/**
 * 堆排序算法
 */
void HeapSort(int H[],int length)
{
    //初始堆
	BuildingHeap(H, length);
	//从最后一个元素开始对序列进行调整
	for (int i = length - 1; i > 0; --i)
	{
		//交换堆顶元素H[0]和堆中最后一个元素
		int temp = H[i]; H[i] = H[0]; H[0] = temp;
		//每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
		HeapAdjust(H,0,i);
  }
} 

int main(){
	int H[10] = {3,1,5,7,2,4,9,6,10,8};
	cout<<"初始值:";
	print(H,10);
	HeapSort(H,10);
	//selectSort(a, 8);
	cout<<"结果:";
	print(H,10);

}

分析:

设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式: 

                               

而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。

5. 交换排序—冒泡排序(Bubble Sort)


基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序的示例:

 

算法的实现:

void bubbleSort(int a[], int n){
	for(int i =0 ; i< n-1; ++i) {
		for(int j = 0; j < n-i-1; ++j) {
			if(a[j] > a[j+1])
			{
				int tmp = a[j] ; a[j] = a[j+1] ;  a[j+1] = tmp;
			}
		}
	}
}

冒泡排序算法的改进

对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。本文再提供以下两种改进算法:

1.设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。

改进后算法如下:

void Bubble_1 ( int r[], int n) {
	int i= n -1;  //初始时,最后位置保持不变
	while ( i> 0) { 
		int pos= 0; //每趟开始时,无记录交换
		for (int j= 0; j< i; j++)
			if (r[j]> r[j+1]) {
				pos= j; //记录交换的位置 
				int tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
			} 
		i= pos; //为下一趟排序作准备
	 } 
}

2.传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。

改进后的算法实现为:

void Bubble_2 ( int r[], int n){
	int low = 0; 
	int high= n -1; //设置变量的初始值
	int tmp,j;
	while (low < high) {
		for (j= low; j< high; ++j) //正向冒泡,找到最大者
			if (r[j]> r[j+1]) {
				tmp = r[j]; r[j]=r[j+1];r[j+1]=tmp;
			} 
		--high;					//修改high值, 前移一位
		for ( j=high; j>low; --j) //反向冒泡,找到最小者
			if (r[j]<r[j-1]) {
				tmp = r[j]; r[j]=r[j-1];r[j-1]=tmp;
			}
		++low;					//修改low值,后移一位
	} 
}

6. 交换排序—快速排序(Quick Sort)


基本思想:

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,

2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的示例:

(a)一趟排序的过程:

(b)排序的全过程

算法的实现:

 递归实现:

void print(int a[], int n){
	for(int j= 0; j<n; j++){
		cout<<a[j] <<"  ";
	}
	cout<<endl;
}

void swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

int partition(int a[], int low, int high)
{
	int privotKey = a[low];								//基准元素
	while(low < high){								    //从表的两端交替地向中间扫描
		while(low < high  && a[high] >= privotKey) --high;  //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
		swap(&a[low], &a[high]);
		while(low < high  && a[low] <= privotKey ) ++low;
		swap(&a[low], &a[high]);
	}
	print(a,10);
	return low;
}


void quickSort(int a[], int low, int high){
	if(low < high){
		int privotLoc = partition(a,  low,  high);  //将表一分为二
		quickSort(a,  low,  privotLoc -1);			//递归对低子表递归排序
		quickSort(a,   privotLoc + 1, high);		//递归对高子表递归排序
	}
}

int main(){
	int a[10] = {3,1,5,7,2,4,9,6,10,8};
	cout<<"初始值:";
	print(a,10);
	quickSort(a,0,9);
	cout<<"结果:";
	print(a,10);

}

分析:

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。

快速排序的改进

在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。算法思想如下:

void print(int a[], int n){
	for(int j= 0; j<n; j++){
		cout<<a[j] <<"  ";
	}
	cout<<endl;
}

void swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

int partition(int a[], int low, int high)
{
	int privotKey = a[low];					//基准元素
	while(low < high){					//从表的两端交替地向中间扫描
		while(low < high  && a[high] >= privotKey) --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
		swap(&a[low], &a[high]);
		while(low < high  && a[low] <= privotKey ) ++low;
		swap(&a[low], &a[high]);
	}
	print(a,10);
	return low;
}


void qsort_improve(int r[ ],int low,int high, int k){
	if( high -low > k ) { //长度大于k时递归, k为指定的数
		int pivot = partition(r, low, high); // 调用的Partition算法保持不变
		qsort_improve(r, low, pivot - 1,k);
		qsort_improve(r, pivot + 1, high,k);
	} 
} 
void quickSort(int r[], int n, int k){
	qsort_improve(r,0,n,k);//先调用改进算法Qsort使之基本有序

	//再用插入排序对基本有序序列排序
	for(int i=1; i<=n;i ++){
		int tmp = r[i]; 
		int j=i-1;
		while(tmp < r[j]){
			r[j+1]=r[j]; j=j-1; 
		}
		r[j+1] = tmp;
	} 

} 



int main(){
	int a[10] = {3,1,5,7,2,4,9,6,10,8};
	cout<<"初始值:";
	print(a,10);
	quickSort(a,9,4);
	cout<<"结果:";
	print(a,10);

}

7. 归并排序(Merge Sort)


基本思想:

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序示例:

 

合并方法:

设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。

  • j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标

  • 若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束

  • //选取r[i]和r[j]较小的存入辅助数组rf
    如果r[i] 否则,rf[k]=r[j]; j++; k++; 转⑵

  • //将尚未处理完的子表中元素存入rf
    如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空
    如果j<=n ,  将r[j…n] 存入rf[k…n] //后一子表非空

  • 合并结束。

//将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n]
void Merge(ElemType *r,ElemType *rf, int i, int m, int n)
{
	int j,k;
	for(j=m+1,k=i; i<=m && j <=n ; ++k){
		if(r[j] < r[i]) rf[k] = r[j++];
		else rf[k] = r[i++];
	}
	while(i <= m)  rf[k++] = r[i++];
	while(j <= n)  rf[k++] = r[j++];
}

归并的迭代算法

1 个元素的表总是有序的。所以对n 个元素的待排序列,每个元素可看成1 个有序子表。对子表两两合并生成n/2个子表,所得子表除最后一个子表长度可能为1 外,其余子表长度均为2。再进行两两合并,直到生成n 个元素按关键码有序的表。

void print(int a[], int n){
	for(int j= 0; j<n; j++){
		cout<<a[j] <<"  ";
	}
	cout<<endl;
}

//将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n]
void Merge(ElemType *r,ElemType *rf, int i, int m, int n)
{
	int j,k;
	for(j=m+1,k=i; i<=m && j <=n ; ++k){
		if(r[j] < r[i]) rf[k] = r[j++];
		else rf[k] = r[i++];
	}
	while(i <= m)  rf[k++] = r[i++];
	while(j <= n)  rf[k++] = r[j++];
	print(rf,n+1);
}

void MergeSort(ElemType *r, ElemType *rf, int lenght)
{ 
	int len = 1;
	ElemType *q = r ;
	ElemType *tmp ;
	while(len < lenght) {
		int s = len;
		len = 2 * s ;
		int i = 0;
		while(i+ len <lenght){
			Merge(q, rf,  i, i+ s-1, i+ len-1 ); //对等长的两个子表合并
			i = i+ len;
		}
		if(i + s < lenght){
			Merge(q, rf,  i, i+ s -1, lenght -1); //对不等长的两个子表合并
		}
		tmp = q; q = rf; rf = tmp; //交换q,rf,以保证下一趟归并时,仍从q 归并到rf
	}
}


int main(){
	int a[10] = {3,1,5,7,2,4,9,6,10,8};
	int b[10];
	MergeSort(a, b, 10);
	print(b,10);
	cout<<"结果:";
	print(a,10);

}

两路归并的递归算法

void MSort(ElemType *r, ElemType *rf,int s, int t)
{ 
	ElemType *rf2;
	if(s==t) r[s] = rf[s];
	else
	{ 
		int m=(s+t)/2;			/*平分*p 表*/
		MSort(r, rf2, s, m);		/*递归地将p[s…m]归并为有序的p2[s…m]*/
		MSort(r, rf2, m+1, t);		/*递归地将p[m+1…t]归并为有序的p2[m+1…t]*/
		Merge(rf2, rf, s, m+1,t);	/*将p2[s…m]和p2[m+1…t]归并到p1[s…t]*/
	}
}
void MergeSort_recursive(ElemType *r, ElemType *rf, int n)
{   /*对顺序表*p 作归并排序*/
	MSort(r, rf,0, n-1);
}

8. 桶排序/基数排序(Radix Sort)

说基数排序之前,我们先说桶排序:

기본 아이디어: 배열을 제한된 수의 버킷으로 나누는 것입니다. 각 버킷은 개별적으로 정렬됩니다(다른 정렬 알고리즘을 사용하거나 재귀적인 방식으로 버킷 정렬을 계속 사용할 수 있음). 버킷 정렬은 비둘기집 정렬의 귀납적 결과입니다. 버킷 정렬은 정렬할 배열 내의 값이 고르게 분포되어 있을 때 선형 시간(Θ(n))을 사용합니다. 그러나 버킷 정렬은 비교 정렬이 아니며 O(n log n) 하한의 영향을 받지 않습니다. ㅋㅋㅋ                                                                              간단히 말하면, 데이터를 버킷으로 그룹화한 다음 각 버킷의 내용을 정렬합니다.

예를 들어, [1..1000] 범위의 n 정수 A[1..n]을 정렬하려는 경우

먼저 버킷을 크기 10 범위로 설정할 수 있습니다. 구체적으로 다음과 같이 설정합니다. B[ 1]은 [1..10]의 정수를 저장하고, 세트 B[2]는 (10..20]의 정수를 저장하고,... 세트 B[i]는 ((i-1)*의 정수를 저장합니다. 10, i*10] , i = 1,2,..100. 총 100개의 버킷이 있습니다.

그런 다음 A[1..n]을 처음부터 끝까지 스캔하고 각 A[i]를 해당하는 버킷에 넣습니다. 버킷 B[j.]. 이때 버블, 선택 또는 빠른 정렬을 사용할 수 있습니다. 마지막으로 각 버킷의 내용을 출력합니다. 각 버킷의 숫자는 작은 것부터 큰 것 순으로 출력되므로 모든 숫자의 시퀀스가 ​​얻어지고 숫자가 균등하게 분포되어 있으면 평균이 있습니다.

가 각 버킷의 숫자에 대해 빠른 정렬을 사용하는 경우 전체 알고리즘의 복잡성은

O(n + m * n/m*log(n/m )) = O(n + nlogn - nlogm)

위 수식에서 알 수 있듯이 m이 n에 가까울수록 버킷 정렬 복잡도는 O(n)에 가깝습니다.

물론 위의 복잡도 계산은 다음을 기반으로 합니다. n의 입력 숫자가 균등하게 분포되어 있다는 가정은 매우 강합니다. 실제 응용에서는 효과가 그다지 좋지 않습니다. 모든 숫자가 동일한 버킷에 속하면 위에서 언급한 일반적인 정렬로 전락하게 됩니다. 정렬 알고리즘은 O(n2)의 시간 복잡도를 가지며 일부 정렬 알고리즘은 O(nlogn)의 시간 복잡도를 갖습니다. 그러나 버킷 정렬은 O(n)의 시간 복잡도를 달성할 수 있습니다.

1) 첫째. 무엇보다도 공간 복잡도가 상대적으로 높고 필요한 추가 오버헤드가 큽니다. 정렬에는 정렬할 배열을 저장하는 배열과 소위 버킷이 되는 배열의 공간 오버헤드가 있습니다. 정렬할 값은 0부터 m-1까지이면 m개의 버킷이 필요하며 이 버킷 배열에는 최소한 m개의 공백이 필요합니다.

2) 두 번째로 정렬할 요소는 특정 범위 내에 있어야 합니다.

버킷 정렬은 분포 정렬입니다. 할당 정렬의 특별한 점은 키 코드를 비교할 필요가 없다는 점이지만, 정렬할 열의 특정 조건을 알고 있어야 한다는 전제가 있습니다.

할당 정렬의 기본 아이디어:

직접 말하면 다중 버킷 정렬을 수행하는 것입니다.

기수 정렬 프로세스에서는 키워드 비교가 필요하지 않지만 "할당" 및 "수집" 프로세스를 통해 정렬이 이루어집니다. 시간 복잡도는 선형 순서(O(n))에 도달할 수 있습니다.

예:

카드의 52장은 슈트와 액면가에 따라 두 개의 필드로 나눌 수 있습니다.

슈트: 클럽< 레드 하트<


2 < 4 < 7 < 9 < K < 포커를 하는 경우 카드는 모양과 액면가에 따라 오름차순으로 정렬되며 다음 순서가 얻어집니다.
즉, 모양이 다른 경우 액면가에 관계없이 카드 2개가 됩니다. 낮은 무늬는 높은 무늬의 카드보다 작습니다. 같은 무늬의 경우에만 크고 작은 관계는 액면가의 크기에 따라 결정됩니다. 이것은 다중 키 정렬입니다.

为得到排序结果,我们讨论两种排序方法。
方法1:先对花色排序,将其分为4 个组,即梅花组、方块组、红心组、黑心组。再对每个组分别按面值进行排序,最后,将4 个组连接起来即可。
方法2:先按13 个面值给出13 个编号组(2 号,3 号,...,A 号),将牌按面值依次放入对应的编号组,分成13 堆。再按花色给出4 个编号组(梅花、方块、红心、黑心),将2号组中牌取出分别放入对应花色组,再将3 号组中牌取出分别放入对应花色组,……,这样,4 个花色组中均按面值有序,然后,将4 个花色组依次连接起来即可。

设n 个元素的待排序列包含d 个关键码{k1,k2,…,kd},则称序列对关键码{k1,k2,…,kd}有序是指:对于序列中任两个记录r[i]和r[j](1≤i≤j≤n)都满足下列有序关系:

                                                              

其中k1 称为最主位关键码,kd 称为最次位关键码     。

两种多关键码排序方法:

多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序,分两种方法:

最高位优先(Most Significant Digit first)法,简称MSD 法

1)先按k1 排序分组,将序列分成若干子序列,同一组序列的记录中,关键码k1 相等。

2)再对各组按k2 排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd 对各子组排序后。

3)再将各组连接起来,便得到一个有序序列。扑克牌按花色、面值排序中介绍的方法一即是MSD 法。

最低位优先(Least Significant Digit first)法,简称LSD 法

1) 先从kd 开始排序,再对kd-1进行排序,依次重复,直到按k1排序分组分成最小的子序列后。

2) 最后将各个子序列连接起来,便可得到一个有序的序列, 扑克牌按花色、面值排序中介绍的方法二即是LSD 法。

基于LSD方法的链式基数排序的基本思想

  “多关键字排序”的思想实现“单关键字排序”。对数字型或字符型的单关键字,可以看作由多个数位或多个字符构成的多关键字,此时可以采用“分配-收集”的方法进行排序,这一过程称作基数排序法,其中每个数字或字符可能的取值个数称为基数。比如,扑克牌的花色基数为4,面值基数为13。在整理扑克牌时,既可以先按花色整理,也可以先按面值整理。按花色整理时,先按红、黑、方、花的顺序分成4摞(分配),再按此顺序再叠放在一起(收集),然后按面值的顺序分成13摞(分配),再按此顺序叠放在一起(收集),如此进行二次分配和收集即可将扑克牌排列有序。   

基数排序:

是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

算法实现:

Void RadixSort(Node L[],length,maxradix)
{
   int m,n,k,lsp;
   k=1;m=1;
   int temp[10][length-1];
   Empty(temp); //清空临时空间
   while(k<maxradix) //遍历所有关键字
   {
     for(int i=0;i<length;i++) //分配过程
    {
       if(L[i]<m)
          Temp[0][n]=L[i];
       else
          Lsp=(L[i]/m)%10; //确定关键字
       Temp[lsp][n]=L[i];
       n++;
   }
   CollectElement(L,Temp); //收集
   n=0;
   m=m*10;
  k++;
 }
}

总结


各种排序的稳定性,时间复杂度和空间复杂度总结:

 我们比较时间复杂度函数的情况:

 

ㅋㅋㅋ                                                                                              시간 복잡도 함수의 성장 O(n)

그러니 더 큰 n으로 레코드를 정렬하세요. 일반적인 선택은 O(nlog2n)의 시간 복잡도를 갖는 정렬 방법입니다.

시간 복잡도 측면에서:

(1) 제곱 순서(O(n2)) 정렬

다양한 유형의 단순 정렬: 직접 삽입, 직접 선택 및 버블 정렬
(2) 선형 로그 순서( O(nlog2n)) 정렬
빠른 정렬, 힙 정렬 및 병합 정렬
(3)O(n1+§)) 정렬, §은 0과 1 사이의 상수입니다.

 힐 정렬

(4) 선형 순서(O(n)) 정렬
 기수 정렬, 버킷 및 빈 정렬에 추가됩니다.

설명:

원본 테이블을 순서대로 정렬하거나 기본적으로 정렬하는 경우 직접 삽입 정렬과 버블 정렬을 사용하면 비교 횟수와 기록 이동 횟수가 크게 줄어들고 시간 복잡도도 O(n)으로 줄어들 수 있으며

빠릅니다. 정렬은 기본적으로 원래 테이블을 정렬하면 버블 정렬로 변질되고 시간 복잡도는 O(n2)로 증가합니다.

원래 테이블의 정렬 여부에 관계 없이 단순 선택 정렬, 힙 정렬, 병합 정렬 기수 정렬 시간 복잡도는 거의 영향을 미치지 않습니다.

안정성:

정렬 알고리즘의 안정성: 정렬할 순서에 동일한 키워드를 가진 레코드가 여러 개 있는 경우, 정렬 후 해당 레코드의 상대 순서 정렬 후 레코드 순서가 변경되면 알고리즘이 불안정하다고 합니다.     안정성의 이점
: 정렬 알고리즘이 안정적인 경우 한 키에서 정렬한 다음 다른 키에서 정렬하면 첫 번째 키 정렬 결과를 두 번째 키 정렬에 사용할 수 있습니다. 기수 정렬은 낮은 비트를 기준으로 정렬한 다음 높은 비트를 기준으로 정렬하는 방식입니다. 또한 정렬 알고리즘이 안정적이면 중복 비교를 피할 수 있습니다.

안정적인 정렬 알고리즘: 버블 정렬, 삽입 정렬, 병합 정렬 및 기수 정렬

안정적인 정렬 알고리즘이 아닙니다: 정렬, 퀵 정렬, 힐 정렬, 힙 정렬을 선택하세요.

정렬 알고리즘 선택 지침:

각 정렬 알고리즘에는 고유한 장점과 단점이 있습니다. 따라서 실제로는 상황에 따라 적절하게 선택하는 것이 필요하며, 여러 가지 방법을 조합할 수도 있습니다.

정렬 알고리즘 선택의 기초

정렬에 영향을 미치는 요소는 많습니다. 평균 시간 복잡도가 낮은 알고리즘이 반드시 최적의 알고리즘은 아닙니다. 반대로, 때로는 평균 시간 복잡도가 높은 알고리즘이 일부 특수한 경우에 더 적합할 수도 있습니다. 동시에 알고리즘을 선택할 때 소프트웨어 유지 관리를 용이하게 하기 위해 가독성도 고려해야 합니다. 일반적으로 고려해야 할 네 가지 요소가 있습니다:

1. 정렬할 레코드 수 n의 크기

2. 레코드 자체의 데이터 볼륨 크기, 즉 키워드를 제외한 레코드 내 다른 정보의 크기

3. 키워드의 구조와 분포

4. 정렬 안정성 요구 사항.

정렬할 요소의 개수가 n개라고 가정합니다.

1)

n이 클 경우 시간 복잡도가 O(nlog2n)인 정렬 방법(퀵 정렬, 힙 정렬 또는 병합 정렬)을 사용해야 합니다. . 퀵 정렬: 현재 비교 기반 내부 정렬 중 가장 좋은 방법으로 간주됩니다. 정렬할 키워드가 무작위로 분포되어 있는 경우 퀵 정렬의 평균 시간이 가장 짧습니다.

힙 정렬: 메모리 공간이 허용되고 요구 사항은 성적으로 안정적입니다.


병합 정렬: 일정량의 데이터 이동이 포함되므로 삽입 정렬과 결합하여 먼저 특정 길이의 시퀀스를 얻은 다음 병합할 수 있으므로 효율성이 향상됩니다.

2)

n이 클 경우 메모리 공간이 허용되고 안정성이 요구됨 => 병합 정렬

3)

n이 작을 경우 직접 삽입 또는 직접 선택 정렬을 사용할 수 있습니다. 직접 삽입 정렬: 요소가 순서대로 분포되어 있는 경우 직접 삽입 정렬을 사용하면 비교 횟수와 이동 레코드 수가 크게 줄어듭니다.

직접 선택 정렬: 요소가 순서대로 배포됩니다. 안정성이 필요하지 않으면 직접 선택 정렬을 선택하세요

5)

일반적으로 전통적인 버블 정렬은 사용되지 않거나 직접 사용되지 않습니다.

6)Radix sort
안정적인 정렬 알고리즘이지만 특정 제한 사항이 있습니다.
1. 키워드를 분해할 수 있습니다.
2. 기록된 키워드는 자릿수가 적고, 조밀할수록 좋습니다.
3. 숫자인 경우 부호가 없는 것이 가장 좋으며, 그렇지 않으면 해당 매핑 복잡성이 증가합니다. 먼저 숫자를 별도로 지정하세요.

더 많은 프로그래밍 관련 지식을 보려면 프로그래밍 소개를 방문하세요! !

위 내용은 8가지 정렬 알고리즘은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.