데이터 구조 - 8가지 정렬

 이 기사의 모든 정렬은 예를 들어 오름차순입니다.

콘텐츠

1. 직접 삽입 정렬

2. 힐 정렬

세 번째, 선택 정렬​

넷째, 힙 정렬

5. 버블 정렬

6. 빠른 정렬

재귀 버전

1. 호아레 버전

2. 굴착 방식

3. 앞 뒤 포인터 방식(이 방법을 추천함)

빠른 정렬 최적화

1. 세 숫자의 중국어 방법

2. 작은 부분구간으로의 재귀 

비재귀 버전

일곱, 병합 정렬

재귀적 구현:

비재귀적 구현:

여덟, 세고 정렬하기 

8가지 안정성 요약:


1. 직접 삽입 정렬

기본 아이디어: 우리가 보통 포커를 할 때 카드 뽑기 단계의 정렬에서 삽입 정렬의 개념을 사용합니다.

1. n번째 요소를 삽입할 때 이전 n-1개의 숫자가 이미 순서대로 있습니다.

2. 이 n번째 숫자를 이용하여 이전의 n-1번 숫자와 비교하여 삽입할 위치를 찾아 삽입합니다. (원래 위치의 숫자는 미리 저장되어 있기 때문에 덮어쓰지 않습니다)

3. 원래 위치의 데이터가 차례로 뒤로 이동합니다.

 구현:

①단방향 구현([0,end]의 정렬된 간격에 x를 삽입)

즉, 일반적으로 삽입을 위해 몇몇 숫자를 무작위로 열거하고 삽입할 숫자를 두 가지 경우로 나눕니다.

(1) 삽입할 번호는 앞의 순서번호의 가운데 번호이며 직접비교는 끝+1 위치에 x를 할당한다.

(2) x는 가장 작은 숫자이고 end는 -1의 위치에 도달하고 마지막으로 x를 end+1 위치에 직접 할당합니다.

 ② 배열 전체 정렬 구현 

배열이 처음에 정렬되었는지 몰랐기 때문에 첨자를 제어하고 end는 0부터 시작하고 항상 end+1 위치의 값을 x에 저장하고 루프는 단일 패스로 정렬할 수 있습니다. 끝에서 end= n-2, n-1 위치의 숫자는 x에 저장됩니다.

 전체 코드:

void InsertSort(int* a, int n)
{
	assert(a);

	for (int i = 0; i < n - 1; ++i)
	{
		int end = i;
		int x=a[end+1];//将end后面的值保存到x里面了
		//将x插入到[0,end]的有序区间
		while (end >= 0)
		{
			if (a[end] > x)
			{
				a[end + 1] = a[end];  //往后挪动一位
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = x;      //x放的位置都是end的后一个位置
	}
	
}

직접 삽입 정렬 요약:

① 요소가 순서에 가까울수록 직접 삽입 정렬의 효율성이 높아집니다. 

②시간 복잡도: O(N^2)

최악의 경우 숫자를 삽입할 때마다 이전 숫자를 이동해야 하므로 총 1+2+3+...+n=n(n+1)/2 

③ 공간복잡도: O(1)

추가 공간이 없고 일정한 수의 변수만 있음

2. 힐 정렬

 

기본 아이디어:

1. 먼저 n보다 작은 수를 갭으로 선택하고, 갭과 거리가 있는 모든 숫자를 그룹화하여 사전 정렬(직접 삽입 정렬)

2. 간격보다 작은 다른 숫자를 선택하고 ①의 작업을 반복합니다.

3. gap=1일 때 배열 전체가 그룹인 것과 같으며, 한번 더 삽입하면 전체 순서를 정렬할 수 있다.

예: 

구현:

①단일 그룹 소트

기존의 직접삽입과 동일, 즉 원래의 간격이 1이 되고 이제 공백이 되며 각 그룹이 별도로 미리 정렬됩니다.

②여러 그룹이 정렬됩니다.

③ 전체 배열 정렬(컨트롤 갭)

다중 사전 정렬(gap>1) + 하나의 삽입 정렬(gap==1)

(1) 간격이 클수록 사전 정렬이 빠르고 순서에 덜 가깝습니다.

(2) 간격이 작을수록 사전 정렬이 느려지고 순서에 가까워집니다.

결과는 다음과 같습니다. 

전체 코드:

void ShellSort(int* a, int n)
{

	int gap = n;
	while (gap > 1)
	{
		gap /= 2;

		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = x;
		}
	}
}

언덕 정렬 요약:

①힐 정렬은 직접 삽입 정렬의 최적화입니다.

②시간 복잡도: O(N^1.3)

③ 공간복잡도: O(1) 

3. 선택 정렬 

기본 아이디어:

매번 배열에서 가장 크거나 작은 것을 선택하고 모두 정렬될 때까지 배열의 맨 오른쪽 또는 맨 왼쪽에 저장합니다. 

구현:

여기에 최적화를 시켰는데, 한번의 소팅에서 가장 큰 수(a[maxi])와 가장 작은 수(a[mini])를 직접 선택하여 맨 오른쪽과 맨 왼쪽에 배치하여 원래의 2배의 정렬 효율을 낸다.

①단품 주문

가장 작은 수(a[mini])와 가장 큰 수(a[maxi])를 찾아 맨 왼쪽과 맨 오른쪽에 배치합니다.

ps: 시작과 끝은 레코드의 왼쪽과 오른쪽 아래 첨자를 저장하고, 미니 및 맥시 레코드는 최소 및 최대 아래 첨자를 저장합니다.

 ② 전체 배열 정렬

begin++와 end-- 다음에 남은 n-2개의 숫자를 배열하고 한번의 패스를 다시 수행하여 begin이 end보다 작을 때까지 루프를 형성할 수 있도록 한다.

전체 코드:

void SelectSort(int* a, int n)
{
	int begin = 0,end = n - 1;

	while (begin<end)
	{
		int mini = begin, maxi = begin;

		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[mini], &a[begin]);
		//当begin==maxi时,最大值会被换走,修正一下
		if (begin==maxi)
		{
			maxi=mini;
		}
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}
}

직접 선택 정렬 요약:

① 직접선택정렬은 잘 이해하고 있으나 실제 효율이 높지 않고 거의 사용되지 않음

②시간 복잡도: O(N^2)

③ 공간복잡도: O(1)

넷째, 힙 정렬

기본 아이디어:

1. 큰 힙으로 정렬할 시퀀스를 구성합니다.큰 힙의 특성에 따라 현재 힙의 루트 노드(힙 상단)가 시퀀스에서 가장 큰 요소입니다.

2. 힙의 맨 위 요소를 마지막 요소로 바꾼 다음 나머지 노드를 큰 힙으로 재구성합니다.

3. 큰 힙의 첫 번째 빌드부터 시작하여 2단계를 반복합니다. 빌드할 때마다 시퀀스의 최대값을 얻은 다음 큰 힙의 맨 뒤에 놓을 수 있습니다. 마지막으로 순서가 지정된 시퀀스를 얻습니다.

작은 결론:

오름차순 정렬, 힙 빌드

내림차순으로 정렬하고 작은 힙을 만듭니다.

구현:,

① 알고리즘 하향 조정

주어진 배열 시퀀스를 큰 힙으로 빌드합니다. 루트 노드에서 힙을 빌드하려면 여러 하향 조정 알고리즘이 필요합니다.

힙 하향 조정 알고리즘(전제 사용):
(1) 작은 힙으로 조정하려면 루트 노드의 왼쪽 및 오른쪽 하위 트리가 모두 작은 힙이어야 합니다.
(2) 큰 힙으로 조정하려면 루트 노드의 왼쪽 및 오른쪽 하위 트리가 큰 힙이어야 합니다.

하향 조정 알고리즘의 기본 아이디어:

1. 루트 노드에서 시작하여 왼쪽 및 오른쪽 자식 값이 큰 노드를 선택합니다. 

2. 선택한 자녀의 값이 아버지의 값보다 크면 두 값을 교환

3. 큰 아이를 새 아버지로 여기고 잎 노드에 도달할 때까지 계속 하향 조정합니다.

//向下调整算法
//以建大堆为例
void AdJustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	//默认左孩子较大
	while (child < n)
	{
		if (child + 1 < n && a[child+1] > a[child ])//如果这里右孩子存在,
                                       //且更大,那么默认较大的孩子就改为右孩子
		{
			child++;
		}
		if(a[child]>a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

②힙 빌드(주어진 임의의 배열을 큰 힙으로 빌드)

힙 구축 아이디어:

끝에서 두 번째 non-leaf 노드부터 시작하여 뒤에서 앞으로 차례로 부모로 사용되며 루트 위치로 조정될 때까지 차례로 아래쪽으로 조정됩니다.

 힙 다이어그램:

    //最后一个叶子结点的父亲为i,从后往前,依次向下调整,直到调到根的位置
	for (int i = (n - 1 - 1) / 2;i>=0;--i)
	{
		AdJustDown(a,n,i);
	}

 ③힙 정렬(힙 삭제 개념을 사용)

힙 정렬의 아이디어:

1. heap이 생성된 후, heap 상단의 숫자를
마지막 숫자로 바꾼다. 2. 마지막 숫자를 보지 말고, 나머지 n-1 숫자를 더미로 조정한 후 1단계로 이동 .

3. 끝에 하나의 숫자만 남을 때까지 정지하여 순서대로 배열

for (int end = n - 1; end > 0; --end)
	{
		Swap(&a[end],&a[0]);
		AdJustDown(a,end,0);
	}

전체 코드는 다음과 같습니다.

void AdJustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	
	while (child < n)
	{
		if (child + 1 < n && a[child+1] > a[child ])
                                       
		{
			child++;
		}
		if(a[child]>a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int*a,int n)
{
	
	for (int i = (n - 1 - 1) / 2;i>=0;--i)
	{
		AdJustDown(a,n,i);
	}
	
	for (int end = n - 1; end > 0; --end)
	{
		Swap(&a[end],&a[0]);
		AdJustDown(a,end,0);
	}
}

5. 버블 정렬

버블 정렬의 기본 아이디어:

한 번의 여행 과정에서 앞과 뒤의 두 숫자를 차례로 비교하고 더 큰 숫자를 뒤로 밀고 다음에는 나머지 n-1 숫자만 비교하면 되는 식이다. 

//优化版本的冒泡排序
void BubbleSort(int* a, int n)
{
	int end = n-1;
	while (end>0)
	{
		int exchange = 0;
		for (int i = 0; i < end; i++)
		{
			if (a[i] > a[i + 1])
			{
				Swap(&a[i], &a[i + 1]);
				exchange = 1;
			}
		}
		if (exchange == 0)//单趟过程中,若没有交换过,证明已经有序,没有必要再排序
		{
			break;
		}
		end--;
	}
}

 버블 정렬 요약:

① 매우 이해하기 쉬운 정렬

②시간 복잡도: O(N^2)

③ 공간복잡도: O(1)

6. 빠른 정렬

재귀 버전

1. 호아레 버전

Hoare의 일방적인 생각:

1. 왼쪽에 키를 만들고 오른쪽으로 먼저 이동하여 키보다 작은 값을 찾습니다.

2. 왼쪽으로 돌아가 키보다 큰 값을 찾습니다.

3. 그런 다음 왼쪽과 오른쪽 값을 바꿉니다.

4. 위의 1 2 3 단계를 항상 반복하십시오.

5. 두 사람이 만났을 때의 위치가 가장 왼쪽에 있는 선택된 키 값으로 교환됩니다.

이렇게 하면 키가 올바른 위치에 놓입니다.

애니메이션 데모:  

//hoare版本
//单趟排序  让key到正确的位置上   keyi表示key的下标,并不是该位置的值
int partion1(int* a, int left, int right)
{
	int keyi = left;//左边作keyi
	while (left < right)
	{   //右边先走,找小于keyi的值
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边后走,找大于keyi的值
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = partion1(a, left, right);
	//[left,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

2. 굴착 방식

사실은 본질적으로 hoare의 변형입니다.

단방향 굴착 방법:

1. 먼저 맨 왼쪽의 첫 번째 데이터를 임시 변수 키에 저장하여 피트를 형성합니다.

2. 오른쪽부터 키보다 작은 값을 찾아 그 값을 구덩이에 던지고 이때 새로운 구덩이를 형성한다.

3. 왼쪽부터 시작하여 키보다 큰 값을 찾아 그 값을 구덩이에 던지고 이때 새로운 구덩이를 형성한다.

4. 1 2 3 단계를 항상 반복하십시오.

5. 양측이 만날 때까지 새로운 구덩이가 형성되고 마침내 그 안에 키 값이 던져진다.

이렇게 하면 키가 올바른 위치에 도달했습니다.

애니메이션 데모: 


//挖坑法
int partion2(int* a, int left, int right)
{
	int key = a[left];
	int pit = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[pit] = a[right];//填坑
		pit=right;


		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[pit] = a[left];//填坑
		pit=left;
	}
	a[pit] = key;
	return pit;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = partion2(a, left, right);
	//[left,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

3. 앞 뒤 포인터 방식(이 방법을 추천함)

전면 및 후면 포인터의 아이디어:

1. 처음에 prev를 시퀀스의 시작으로 선택하고 cur 포인터는 prev의 다음 위치를 가리키고 왼쪽의 첫 번째 숫자를 키로 선택합니다.

2. Cur가 먼저 실행되고 키보다 작은 값을 찾은 다음 발견되면 중지합니다.

3、++이전

4. prev와 cur를 첨자 값으로 바꿉니다.

5. 루프에서 2 3 4 단계를 반복합니다.멈춘 후 마지막으로 키와 prev를 아래 첨자 값으로 교환합니다.

이렇게 하면 키도 올바른 위치에 도달합니다.

애니메이션 데모: 

int partion3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//prev != cur  防止cur和prev相等时,相当于自己和自己交换,可以省略
		{                                   //前置 ++ 的优先级大于 != 不等于的优先级
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = partion3(a, left, right);
	//[left,keyi-1] keyi [keyi+1,right]
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

재귀 확장 그래프

빠른 정렬 최적화

1. 세 숫자의 중국어 방법

퀵정렬은 데이터에 민감하므로 순서가 매우 무질서하고 혼돈되면 퀵정렬의 효율성이 매우 높지만 순서가 순서대로라면 시간복잡도는 O(N*logN)에서 O(N)으로 변한다. ^2), 버블 정렬과 동일

각각의 정렬을 위해 선택한 키가 정확히 시퀀스의 중간 값인 경우, 즉 키가 단일 정렬 후 시퀀스의 중간에 위치하면 빠른 정렬의 시간 복잡도는 O(NlogN)

그러나 이것은 이상적인 상황입니다.우리가 정렬된 배열인 극단적인 경우 시퀀스 집합에 직면했을 때 왼쪽을 키 값으로 선택하면 O(N^2) 복잡성으로 퇴화하므로 이때 첫 번째 위치, 꼬리 위치, 중간 위치에 있는 숫자를 3개의 숫자로 선택하고 중간 위치에 있는 숫자를 선택하여 맨 왼쪽에 놓는다. 최적화 후 모든 것이 이상적인 상황이 됩니다.

//快排的优化
//三数取中法
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	
	if (a[left] < a[right])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}

	else
	{
		
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[mid] < a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}

}
int partion5(int* a, int left, int right)
{
	//三数取中,面对有序时是最坏的情况O(N^2),现在每次选的key都是中间值,变成最好的情况了
	int midi = GetMidIndex(a, left, right);
	Swap(&a[midi], &a[left]);//这样还是最左边作为key

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)//prev != cur  防止cur和prev相等时,相当于自己和自己交换,可以省略
		{                                   //前置 ++ 的优先级大于 != 不等于的优先级
			//++prev;
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

2. 작은 부분구간으로의 재귀 

recursion depth가 커질수록 recursion 횟수는 layer당 2배씩 증가하여 효율성에 큰 영향을 미치며, sorting할 sequence의 길이를 일정 크기로 나누었을 때, recursion을 계속하는 효율성 분할은 삽입 정렬보다 나쁩니다. 빠른 행 대신 삽입 행을 사용할 수 있습니다.

분할된 간격의 길이가 10보다 작을 때 삽입 정렬을 사용하여 나머지 숫자를 정렬할 수 있습니다.

//小区间优化法,可以采用直接插入排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		int keyi = partion5(a, left, right);
		//[left,keyi-1] keyi [keyi+1,right]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

비재귀 버전

재귀 알고리즘은 주로 하위 간격을 나눕니다.비재귀적으로 빠른 정렬을 구현하려면 스택을 사용하여 간격을 저장하기만 하면 됩니다. 일반적으로 재귀 프로그램을 비재귀 프로그램으로 변경할 때 가장 먼저 떠오르는 것은 재귀 자체가 스택을 푸시하는 과정이기 때문에 스택을 사용하는 것입니다.

비재귀의 기본 아이디어:

1. 정렬된 배열의 시작 위치와 끝 위치를 저장할 스택을 신청합니다.

2. 전체 어레이의 시작 및 끝 위치를 스택으로 밀어 넣습니다.

3. 스택의 특성은 후입선출(Last In, First Out)이므로 오른쪽이 스택으로 다시 밀려나기 때문에 오른쪽이 먼저 스택에서 나옵니다.

스택의 맨 위 요소를 받아서 스택을 팝하는 끝을 정의하고, 스택의 맨 위 요소를 받아서 스택을 팝하는 시작을 정의합니다.

4. 배열을 단일 패스로 정렬하고 키 값의 첨자를 반환합니다.

5. 이때 기준값 키의 왼쪽에 순서를 배열해야 합니다.

참조 값 키의 왼쪽에 있는 시퀀스의 시작 위치와 끝 위치만 스택에 저장되어 있으면 왼쪽 정렬 시 다음 간격을 찾지 않습니다. 따라서 먼저 오른쪽 시퀀스의 시작 위치와 끝 위치를 스택에 저장한 다음 왼쪽 시퀀스의 시작 위치와 끝 위치를 스택에 저장합니다.

6. 스택이 비어 있는지 확인하고, 비어 있지 않으면 4, 5단계를 반복합니다. 비어 있으면 정렬이 완료된 것입니다.

void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st,left);
	StackPush(&st, right);

	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);
		StackPop(&st);

		int begin = StackTop(&st);
		StackPop(&st);

		int keyi = partion5(a,begin,end);
		//区间被成两部分了 [begin,keyi-1] keyi [keyi+1,end]
		if (keyi + 1 < end)
		{
			StackPush(&st,keyi+1);
			StackPush(&st,end);
		}
		if (keyi-1>begin)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi -1);
		}
	}
	StackDestroy(&st);
}

빠른 정렬 요약:

① 퀵소트의 전체적인 종합적인 성능과 사용 시나리오는 비교적 양호하므로 감히 퀵소트라고 부른다.

②빠른 정렬의 유일한 막다른 길은 2,3,2,3,2,3,2,3과 같이 정렬되거나 거의 정렬된 시퀀스를 정렬하는 것이며, 이는 O(N^2) 시간 복잡도가 됩니다.

③시간복잡도 O(N*logN)

④ 공간복잡도 O(logN)

일곱, 병합 정렬

 병합 정렬의 기본 개념(분할 및 정복 아이디어):

1. ( 분할 ) 배열을 왼쪽 시퀀스와 오른쪽 시퀀스로 나누고 둘 다 따로 정렬한 다음 왼쪽 시퀀스를 왼쪽 시퀀스와 오른쪽 시퀀스로 세분하고 이 단계를 하위 시퀀스에 존재하지 않을 때까지 반복합니다. 간격 또는 숫자가 하나만 있을 때까지

2. ( Merge ) 첫 번째 단계에서 얻은 숫자를 정렬된 간격으로 결합

 구현:

① 분할

 ②병합

재귀적 구현:

이것은 아이디어에서 이진 트리와 매우 유사하므로 재귀 메서드를 사용하여 병합 정렬을 구현할 수 있습니다.

코드 쇼는 아래와 같습니다.

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid+1, right, tmp);
	
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}
}
//归并排序
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a,0,n-1,tmp);

	free(tmp);
	tmp = NULL;
}

비재귀적 구현:

재귀적 구현의 단점은 스택이 항상 호출되고 스택 메모리가 종종 매우 작다는 것입니다. 그래서 우리는 루프 방법을 사용하여

배열의 첨자를 조작하기 때문에 위에서 재귀적으로 얻은 배열 첨자를 저장하는 데 도움이 되는 배열을 사용해야 합니다.재귀와의 차이점은 재귀는 항상 간격을 세분화해야 하고 왼쪽 간격은 다음과 같아야 한다는 것입니다. 재귀 적으로 나눕니다. , 다음 재귀 적으로 오른쪽 간격을 나누고 배열의 비 재귀는 데이터를 한 번에 처리하고 매번 첨자를 다시 원래 배열로 복사하는 것입니다.

병합 정렬의 기본 아이디어는 정렬할 시퀀스 a[0...n-1]를 길이가 1인 n개의 정렬된 시퀀스로 간주하고 인접한 정렬된 목록을 쌍으로 병합하여 n/2개의 길이 2를 얻는 것입니다. ;의 정렬된 목록은 이러한 정렬된 시퀀스를 다시 병합하여 길이가 4인 n/4개의 정렬된 시퀀스를 얻고, 마지막으로 길이가 n인 정렬된 시퀀스를 얻습니다.

그러나 우리는 이상적인 상황(짝수)에 있고 특별한 경계 통제가 있습니다.데이터의 수가 짝수가 아닐 때 우리가 나누는 갭 그룹은 경계를 벗어난 위치를 가질 수밖에 없습니다.

첫 번째 경우:

두 번째 경우:

코드 쇼는 아래와 같습니다.

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			// [i,i+gap-1] [i+gap,i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 核心思想:end1、begin2、end2都有可能越界
			// end1越界 或者 begin2 越界都不需要归并
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			
			// end2 越界,需要归并,修正end2
			if (end2 >= n)
			{
				end2 = n- 1;
			}

			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

			// 把归并小区间拷贝回原数组
			for (int j = i; j <= end2; ++j)
			{
				a[j] = tmp[j];
			}
		}

		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}

병합 정렬 요약:

①단점은 O(N) 공간 복잡도를 필요로 하고, 오프 디스크 정렬 문제를 해결하기 위해 병합 정렬이 더 많다는 것입니다.

②시간 복잡도: O(N*logN)

③ 공간복잡도: O(N)

여덟, 세고 정렬하기 

비비교 정렬이라고도 하며 비둘기 구멍 원리라고도 하는 해시 직접 주소 지정 방법의 변형 응용 프로그램입니다.

기본 아이디어:

1. 동일한 요소의 발생 횟수 계산

2. 통계 결과에 따라 데이터를 원래 배열로 다시 복사합니다.

구현:

① 동일한 요소의 발생 횟수를 센다.

주어진 임의의 배열 a에 대해 count 배열 count를 열어야 합니다. a[i]는 몇 개이고 count 배열의 첨자는 몇 개 ++입니다.

여기서 우리는 절대 매핑을 사용합니다. 즉, a[i]의 배열 요소가 몇 개이고 count 배열의 첨자가 몇 ++인 위치에 있지만 데이터 집계의 경우 다음에서 시작하지 않습니다. 1001과 같은 더 작은 숫자, 1002, 1003, 1004와 같은 데이터의 경우 상대 매핑 방법을 사용하여 배열 공간을 여는 낭비를 피할 수 있습니다. count 배열의 공간 크기는 최소값을 빼서 결정할 수 있습니다. 배열의 최대값에서 value + 1(예: range=max-min+1), count 배열 첨자 j =a[i]-min을 얻을 수 있습니다.

 ② count 배열의 결과에 따라 배열에 데이터를 다시 복사합니다.

count[j]의 데이터 개수는 해당 숫자가 몇 번 등장했는지를 나타내는 것으로 0이면 복사할 필요가 없다.

 코드 쇼는 아래와 같습니다.

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];//如果不赋值,min和max就是默认随机值,最好给赋值一个a[0]

	for (int i=1;i<n;i++)//修正 找出A数组中的最大值和最小值
	{
		if (a[i] < min)
		{
			min=a[i];
		}
		if (a[i]>max)
		{
			 max=a[i];
		}
	}
	int range = max - min + 1;//控制新开数组的大小,以免空间浪费
	int* count = (int*)malloc(sizeof(int) * range);
	memset(count,0, sizeof(int) * range);//初始化为全0
	if (count==NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	//1、统计数据个数
	for (int i=0;i<n;i++)
	{
		count[a[i]-min]++;
	}
	//2、拷贝回A数组
	int j = 0;
	for (int i=0;i<range;i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
	free(count);
	count = NULL;
}

 계산 정렬 요약:

①데이터 범위가 상대적으로 집중되면 효율성이 매우 높지만 사용 시나리오가 매우 제한적이며 음수를 배열할 수 있지만 부동 소수점 수에 대해서는 아무 것도 할 수 없습니다.

②시간 복잡도: O(MAX(N,range))

③ 공간 복잡도: O(범위)

8가지 안정성 요약:

안정정렬은 직접삽입정렬, 버블정렬, 병합정렬

불안정한 정렬에는 힐 정렬, 선택 정렬, 힙 정렬, 빠른 정렬, 개수 정렬이 포함됩니다.

추천

출처blog.csdn.net/weixin_57675461/article/details/121903270