데이터 구조 --- 빠른 정렬


데이터 구조-기타 정렬 방법 (정렬 1)
link : link .

1. 빠른 정렬

Quick sort는 Hoare가 1962 년에 제안한 이진 트리 구조 교환 정렬 방법입니다. 기본 개념은 정렬 할 요소 시퀀스의 모든 요소를 ​​기준 값으로 가져 와서 정렬 할 집합을 두 개의 하위 시퀀스로 나누는 것입니다. 정렬 코드에., 왼쪽 하위 시퀀스의 모든 요소가 참조 값보다 작고 오른쪽 하위 시퀀스의 모든 요소가 참조 값보다 큰 다음 모든 요소가 해당 위치에 정렬 될 때까지 맨 왼쪽 하위 시퀀스에 대해 프로세스가 반복됩니다. 위치. ( 간단히 말하면 : 주어진 배열에서 가장 왼쪽 또는 가장 오른쪽 숫자를 K 값으로 선택한 다음 K 값을 적절한 위치에 놓습니다. 오름차순으로 왼쪽 숫자가 K보다 작은 지 확인하십시오. 그리고 오른쪽 숫자가 K보다 크면 K의 왼쪽 절반에서 반복 형식과 유사한 유사한 프로세스가 왼쪽과 오른쪽이 순서대로 정렬 될 때까지 전체가 순서대로 수행 됩니다.

1.1 호 아레 방식 (좌우 포인터 방식)

어려움 :

1. K의 값을 편리하게 찾으려면 일반적으로 배열의 가장 왼쪽 번호 또는 가장 오른쪽 번호를 선택합니다 (최적화도 가능).

2. K보다 큰 값 찾기 시작하고 K보다 작은 값 찾기 종료

3. K의 반대 방향으로의 첫 번째 이동은 해당 숫자를 찾는 것입니다 (오른쪽에서 K를 선택한 경우 먼저 시작을 놓은 다음 시작과 끝이 만나는 지점이 K보다 커야합니다. 원하는 효과를 얻기 위해 K 값이이 위치의 값으로 교환되는지 확인합니다. 왼쪽은 K 값보다 작고 오른쪽은 K 값보다 큽니다.)

여기에 사진 설명 삽입
그렇다면 오른쪽의 K 값을 선택하고 K보다 큰 값을 찾기 시작하는 이유는 무엇입니까? 이 순서를 변경할 수 있습니까? , 오른쪽 끝을 먼저 이동시키고 K보다 작은 숫자
여기에 사진 설명 삽입
를 찾습니다. 이때 시작과 끝이 만나는 위치의 값이 K의 값으로 교환되는 것을 알 수 있습니다. K의 값은 올바른 위치에 있지 않음 (당신의 권리가 모든 값이 K보다 크지 않기 때문입니다)

그러면 왼쪽이 K 값보다 작고 오른쪽이 K 값보다 크지 만 순서가 아니므로이 때 문제를 푸는 것은 재귀 형식과 동일하며 K의 왼쪽 절반은 값을 다시 선택할 수 있습니다. A K 값, 같은 생각, 왼쪽 절반에 주문하고 오른쪽 절반에 주문하면 전체가 순서대로 정렬되어 있습니다.

1.1.1 시간 복잡도 분석

선택한 K 값이 매번 중앙값 (최상의 경우)이면 순회가 이진 트리의 사전 주문 순회와 동일하고 트리의 높이가 logN (기본 2)이면 N 개의 레이어가 있습니다. 시간 복잡도는 O (N * logN) 입니다. 매번 선택하는 숫자가 가장 크거나 가장 작을 때 (최악의 경우), 각 레이어는 N에 가까우며 시간은 O (N * N) 이므로 시간 복잡도는 O (N * logN) ~ O (N * N) 사이입니다.

1.1.2 세 번째 숫자의 중간 가져 오기 (빠른 정렬 최적화)

여기에서 귀하의 기간이 주문 또는 주문에 가까워 질 때 최적화로 이어집니다. K 값은 최소값 또는 최대 값, 시간 복잡도는 O (N * N) 입니다. 선택한 K 값이 가깝기를 바랍니다. K 값을 선택하는 과정에서 중간에 가까운 K 값을 선택할만큼 운이 좋지 않을 수 있지만 적어도 그가 선택한 값이 가장 작거나 크지 않아야합니다. 의. 이런 식으로 최악의 상황을 어느 정도 피하고 질서 정연한 상황에서 최선의 상황이 될 것을 보장 할 수있다

int GetMidIndex(int* a, int begin, int end)
{
    
    
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
    
    
		if (a[mid] < a[end])
			return mid;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else //a[begin] >a[mid]
	{
    
    
		if (a[mid] > a[end])
			return mid;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}

* 세 번째 숫자의 최적화로 빠른 정렬의 시간 복잡도는 더 이상 최악의 경우를 고려하지 않고 시간 복잡도 기호는 O (N * logN)라고 생각합니다.

1.1.3 완전한 코드

//这里写的只是你选择一次K遍历整个数组出来的结果
int PartSort1(int* a, int begin, int end)
{
    
    
	//选出合适的中位数
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);

	int keyIndex = end;
	while (begin < end)
	{
    
    
		//先让begin去找大于K的值
		while (begin<end && a[begin] <= a[keyIndex])
		{
    
    
			++begin;
		}

		//再让end去找比K小的值,此时在这里还要对end有限制条件,不然begin和end在内部循环的时候就错过了,而不是相遇停止
		while (begin<end && a[end] >= a[keyIndex])
		{
    
    
			--end;
		}
		Swap(&a[begin], &a[end]);
	}
	//此时你的begin和end在同一个位置了(选begin还是和end都是一样的),在交换
	Swap(&a[begin], &a[keyIndex]);

	//此时你的begin和end所停留的位置就是K的正确位置
	return begin;
}

//快排
void QuickSort(int* a, int left, int right)
{
    
    
	assert(a);
	//if (left < right) //连等于的时候都可以不用排了,因为此时就剩下一个值了
	//{
    
    
	//	int div = PartSort1(a, left, right);
	//	//递归下去
	//	//[left, div - 1] div [div+1,right]
	//	QuickSort(a, left, div - 1);
	//	QuickSort(a, div + 1, right);
	//}

	if (left >= right)
		return;
	int div = PartSort1(a, left, right);
	//递归下去
	//[left, div - 1] div [div+1,right]
	QuickSort(a, left, div - 1);
	QuickSort(a, div + 1, right);
}

1.2 파기 방법 (핵심 해결 방법은 이해하기 쉬움)

이것은 기본적으로 생각의 좌, 우 포인터 방식과 동일하며, K 값의 반대 방향을 선택한 사람이 그것을 이해할 때 먼저 가도록하는 사고 과정을 피하는 것입니다.
파기 방법 : 먼저 가장 왼쪽 또는 가장 오른쪽을 K로 선택하고, 여기서는 가장 오른쪽이 K로 선택된 것으로 가정합니다. 이때 K 값을 빼낸 후이 위치를 구덩이로 한 다음 시작합니다. K보다 큰 값을 찾아서 구멍을 채 웁니다. 이때이 위치는 구멍입니다. 맨 오른쪽에 끝을 정의하여 K보다 작은 값을 찾습니다. 그런 다음 이전 구멍을 채우고 시작을 놓으십시오. 시작과 끝이 만날 때까지이 구덩이를 선택하고 K 값을 입력하십시오.
여기에 사진 설명 삽입

//挖坑法
int PartSort2(int* a, int begin, int end)
{
    
    
	//选出合适的中位数
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);

	//最开始的坑
	int key = a[end];
	while (begin < end)
	{
    
    
		//让begin去找大于K的值 ,  
		//这里值得注意的就是一定要加=,因为当begin所走到的那个数如果此时和你的key值相等,要么留在左边,要么留在右边都可以
		//但是你不加,就会造成死循环
		while(begin < end && a[begin] <= key)
		{
    
    
			++begin;
		}
		a[end] = a[begin];

		//让end去找比K小的值
		while (begin < end && a[end] >= key)
		{
    
    
			--end;
		}
		a[begin] = a[end];
	}
	//走到这里说明begin和end 相遇了,这里放上K的值
	a[begin] = key;
	return begin;
}

1.3 정방향 및 역방향 포인터 방법

prev와 cur을 정의하고, cur이 키보다 작은 값을 찾도록하고, 그것을 찾으면 멈춘 다음 ++ prev, cur이 배열을 마칠 때까지 두 값을 교환합니다. ++ prev에서 let prev 값과 키 교환 . (이때 왼쪽이 키 값보다 작고 오른쪽이 키 값보다 큽니다)
여기에 사진 설명 삽입
잘 작성하면 코드가 매우 중복됩니다.

//前后指针法
int PartSort3(int* a, int begin, int end)
{
    
    
	//选出合适的中位数
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);
	
	int prev = begin - 1;
	int cur = begin;
	int keyIndex = end;
	while (cur <= end) //
	{
    
    
		//让cur去找小于K的值 ,但是你会发现有的时候你的cur和prev在同一个位置,所以可以不考虑交换
		if (a[cur] < a[keyIndex] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		//当a[cur]比a[keyIndex]小的时候,停下++完prev之后,cur要在++
		//当a[cur]比a[keyIndex]大的时候,直接++cur,所以宗的来说,都是要进行++cur的操作的,所以可以直接合并
		++cur;	
	}
	Swap(&a[++prev], &a[keyIndex]);
	return prev;
}

1.4 간격 최적화

여기에 순서가 아닌 숫자가 10 개만 남아 있어도 계속 반복해야합니다. 먼저 K를 선택한 다음 왼쪽 행이 정렬되고 오른쪽 행이 정렬되고 마지막으로 10 개의 숫자가 정렬됩니다. 사실 이것은 재귀를 위해 중복됩니다. 최적화하는 데 도움이 될뿐만 아니라 더 복잡합니다. 재귀 과정에서 스택 핀을 지속적으로 빌려야하기 때문입니다. 정렬해야하는 배열이 When 일 때 그것은 매우 크고 스택 공간이 오버플로되므로 여기에 최적화가 도입되었습니다. 특정 숫자 범위에 도달하면 재귀 방법이 더 이상 사용되지 않지만 직접 삽입 방법을 사용하여 남은 숫자 정렬을 달성합니다. 기본적으로 차이가 없습니다.
여기에 사진 설명 삽입

//插入排序
void InsertSort(int* a, int n)
{
    
    
	assert(a);
	//把第一个数当成有序的,然后拿后面的数据和第一个数据比较插入
	for (int i = 0; i < n - 1; ++i)
	{
    
    
		//可以把任何一种排序都分解开来为单步分析在整合整体的过程来思考问题
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0) //当这个tmp和第一个值相比较还是小的话,再次移动end就会发现他已经到-1的位置了,说明他依旧和所有的元素都比较过了
		{
    
    
			if (tmp < a[end])
			{
    
    
				a[end + 1] = a[end];
				--end;
			}
			else
			{
    
    
				break;
			}
		}
		//这里跳出来会有两种可能,第一个就是tmp此时大于end下角标所在的元素,break出来
		//第二种就是while循环结束end已经到了-1的位置,(也就是比第一个值还要小)
		a[end + 1] = tmp;
	}
}

//快排
void QuickSort(int* a, int left, int right)
{
    
    
	assert(a);
	if (left >= right)
		return;
 //小区间优化--不再使用递归的方式,而是改用直接插入排序
	if ((right - left + 1) > 10)
	{
    
    
		int div = PartSort3(a, left, right);
		//递归下去
		//[left, div - 1] div [div+1,right]
		QuickSort(a, left, div - 1);
		QuickSort(a, div + 1, right);
	}
	else
	{
    
    
		//区间小于10个数的时候,不再使用递归,而是改用直接插入的方法
		//然而你这里需要排序的数组范围,不再是最初的,可能是此时递归到某一层的左右子树
		InsertSort(a + left, right - left + 1);
	}
}

1.5 비재 귀적 빠른 정렬

아직 C ++를 배우지 않았기 때문에 여전히 스택을 수동으로 작성해야하며 스택 을 사용하여 빠른 정렬 및 비 재귀를 구현해야합니다 . 재귀의 본질은 스택 핀의 도움으로 매개 변수를 저장하는 것이며 여기에서 구현되는 주요 아이디어는 스택 핀의 간격을 스택에 저장하는 것입니다.

비 재귀의 의미 :

① 스택 핀을 재귀 적으로 구축하는 것은 여전히 ​​비용이 많이 들지만 최신 컴퓨터의 경우이 최적화는 최소화되며 무시할 수 있습니다.
② 재귀의 가장 큰 단점은 스택 바늘의 깊이가 너무 깊으면 스택이 넘칠 수 있다는 것입니다. 시스템 스택 공간은 일반적으로 M 수준에서 크지 않지만 데이터 구조 스택은 비 재귀를 시뮬레이션하기 때문에 데이터는 힙에 저장되고 힙은 G 수준에 있습니다.

여기에 사진 설명 삽입

void StackInit(Stack* pst)
{
    
    
	assert(pst);
	//这种方式是有不好的地方的,因为但当你需要增容的时候,你就会发现,他的capacity初始化是0,那么你乘2依旧是0,所以建议一开始就给一个固定值
	//pst->_a = NULL;
	//pst->_top = 0;
	//pst->_capacity = 0;
	pst->_a = (STDateType*)malloc(sizeof(STDateType)*4);
	pst->_top = 0;
	pst->_capacity = 4;
}

//销毁
void StackDestory(Stack* pst)
{
    
    
	assert(pst);
	free(pst->_a);
	pst->_a = NULL;
	pst->_top = pst->_capacity = 0;
}

//入栈
void StackPush(Stack* pst, STDateType x)
{
    
    
	assert(pst);
	//空间不够则增容
	if (pst->_top == pst->_capacity)
	{
    
    
		pst->_capacity *= 2;
		STDateType* tmp = (STDateType*)realloc(pst->_a, sizeof(STDateType)*pst->_capacity);
		if (tmp == NULL)
		{
    
    
			printf("内存不足\n");
			exit(-1);
		}
		else
		{
    
    
			pst->_a = tmp;
		}
	}
	pst->_a[pst->_top] = x;//你所定义的栈顶总是在你放入数据的下一个位置
	pst->_top++;
}

//出栈
void StackPop(Stack* pst)
{
    
    
	assert(pst);
	assert(pst->_top > 0);
	--pst->_top;
}

//获取数据个数
int StackSize(Stack* pst)
{
    
    
	assert(pst);
	return pst->_top;
}


//返回1是空,返回0是非空
int StackEmpty(Stack* pst)
{
    
    
	assert(pst);
	return pst->_top == 0 ? 1 : 0;
}


//获取栈顶的数据
STDateType StackTop(Stack* pst)
{
    
    
	assert(pst);
	assert(pst->_top > 0);
	return pst->_a[pst->_top - 1];//你所定义的栈顶总是在你放入数据的下一个位置
}




//快排的非递归
void QuickSortNonR(int* a, int left, int right)
{
    
    
	Stack st;
	StackInit(&st);

	//先入右在入左
	//那么出的时候就会先出左在出右
	StackPush(&st, right);
	StackPush(&st, left);
	while (!StackEmpty(&st))
	{
    
    
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);
		//此时相当于我们拿到这这段区间[begin,end]
		//先进行大区间单趟排
		int div = PartSort3(a, begin, end);
		//[begin,div-1] div [div+1,end]
		//原来是对[begin,div-1] 和[div+1,end]进行递归的操作,但是现在思路不变但换成用栈来实现
		//这里为了好理解对照着二叉树的前序遍历的思想,选择了先入右在入左,这样出栈以后就可以先堆最左边区间进行操作
		//入右边
		if (div + 1 < end)//当你是一个数的时候就相当于有序了,不再入栈了
		{
    
    
			StackPush(&st, end);
			StackPush(&st, div+1);
		}
		//入左边
		if (begin < div - 1) 
		{
    
    
			StackPush(&st, div-1);
			StackPush(&st, begin);
		}
	}
	StackDestory(&st);
}

1.6 빠른 정렬 기능 요약

  1. 빠른 정렬의 전반적인 성능 및 사용 시나리오는 상대적으로 양호하므로 감히 빠른 정렬을 호출하겠습니다.
  2. 시간 복잡도 : O (N * logN)
  3. 공간 복잡성 : O (logN)
  4. 안정성 : 불안정
    여기에 사진 설명 삽입

추천

출처blog.csdn.net/MEANSWER/article/details/113104009