[Структура данных — ручной алгоритм сортировки, часть 6] Рекурсивная реализация быстрой сортировки (интеграция версии Холла, метода копания, метода переднего и заднего указателя в один метод реализации, очень хорошо печатает)

Оглавление

1. Общие алгоритмы сортировки

1.1 Основная идея биржевой сортировки

2. Реализация метода быстрой сортировки

2.1 Основная идея

3 хора (Холл) версия

3.1 Идеи реализации

3.2 Схема идей

3.3 Почему шаги 2 и 3 идеи реализации нельзя поменять местами

3.4 реализация кода версии hoare

Тест кода версии 3.5 hoare

4. Метод копания

4.1 Идеи реализации

4.2 Схема мышления

4.3 Кодовая реализация метода копания

4.4 Проверка кода метода копания

5. Версии с передним и задним указателем

5.1 Идеи реализации

5.2 Схема мышления

5.3 Кодовая реализация метода переднего и заднего указателя

5.4 Проверка кода до и после метода указателя

6. Анализ временной сложности

6.1 В лучшем случае

6.2 Наихудший случай

7. Оптимизированная быстрая сортировка

7.1 Оптимизация выбора ключей

7.2 Межсотовая оптимизация


1. Общие алгоритмы сортировки

1.1 Основная идея биржевой сортировки

Пузырьковая сортировка — это одна из обменных сортировок, давайте сначала разберемся со следующими идеями пузырьковой сортировки.

Основная идея: Так называемый обмен заключается в том, чтобы поменять местами две записи в последовательности в соответствии с результатом сравнения ключевых значений двух записей в последовательности.Записи с меньшими значениями ключа перемещаются в впереди последовательности.

2. Реализация метода быстрой сортировки

Рекурсивная реализация очень похожа на обход бинарного дерева в прямом порядке.Существуют три распространенных способа разделить интервал на левую и правую половины: 1. вариант Хоара, 2. метод копания, 3. метод левого и правого указателя.

2.1 Основная идея

Быстрая сортировка — это метод обменной сортировки по структуре бинарного дерева, предложенный Хоаром в 1962 году. Его основная идея заключается в следующем: любой элемент в последовательности сортируемых элементов принимается
за эталонное значение, а сортируемое множество делится на две подпоследовательности в соответствии с к коду сортировки все элементы в левой подпоследовательности меньше опорного значения,
все элементы в правой подпоследовательности больше опорного значения, а затем самая левая подпоследовательность повторяет процесс до тех пор, пока все элементы не будут расположены в соответствующей позиции .

Поэтому очень важный момент сзади — идти налево, чтобы найти маленькое, и направо, чтобы найти большое.

3 хора (Холл) версия

3.1 Идеи реализации

Указываем сортировать в порядке возрастания , имя сортируемого массива — a, ключ опорного значения, нижний индекс опорного значения keyi, левое левое, правое право.

1. Выберите ключ.Ключом может быть любой элемент массива, который нужно отсортировать.В этой статье мы выбираем ключ как a[left] ;

2. Идти влево от правого конца (хвоста) массива, когда a[right] < key(a[keyi]), right останавливается;

3. Затем идите вправо от левого конца (головы) массива, когда a[left] > key(a[keyi]), left останавливается;

4. Поменять местами [левый], [правый];

5. Повторите шаги 2, 3 и 4. Когда левый и правый перейдут в одно и то же положение, поменяйте местами элемент и ключ в этом положении, и положение ключа будет определено.

6. В это время ключ делит массив на левый и правый интервалы.Используем первые 5 шагов для левого и правого интервалов, а затем снова определяем ключи левого и правого интервалов, рекурсируем левый интервал, а затем повторите правильный интервал для достижения сортировки.

Примечание. Порядок шагов 2 и 3 изменить нельзя. А почему, мы поговорим об этом после построения диаграммы.

3.2 Схема идей

Наши схемы построены в соответствии с идеями реализации.

3.3 Почему шаги 2 и 3 идеи реализации нельзя поменять местами

Мы можем видеть, что на мысленной диаграмме, когда наконец будет найдено 3, R пойдет первым, а R первым встретится с 3. Если L пойдет первым, ищите большую левую, и L не остановится, когда встретит 3, и перейдет непосредственно к позиции R, позиция R равна 9, когда они встретятся в это время, поменяйте их местами. После обмена не все левые интервалы меньше, чем ключ (6), и произойдет ошибка. Следовательно, если клавиша выбрана слева, сначала пойдет правая.

Q: Мы понимаем версию hoare.Если клавиша выбрана справа, левая идет первой?

Ответ : Да. На самом деле это вопрос о том, встречается ли L с R или R с L. Мы все еще можем использовать приведенное выше решение, чтобы подумать об этой проблеме. Когда L встречается с R, это означает, что R остановился, а R останавливается на [правой] <клавише. В это время, когда L встречается с R, позиция встречи меньше чем key , обмен (key, a[left]), в это время последний элемент, меньший, чем key, помещается в крайнее левое положение, а key находится в определенной позиции, нет необходимости снова настраивать, левый интервал с ключом в качестве средняя точка все меньше ключа, правый интервал больше ключа;

Ключ находится справа. Когда R встречается с L, это означает, что L остановился. Когда L останавливается, это означает, что [левый]> ключ. В это время, когда R встречается с L, позиция встречи больше, чем ключ. Обмен (a[left],key), то последний элемент больше ключа помещается в крайнее правое положение, а ключ находится в определенной позиции, поэтому нет необходимости настраивать его снова.Левый интервал с ключом как средняя точка все меньше ключа, а правый интервал больше ключа.

Таким образом, если клавиша выбрана слева, сначала идите направо; если клавиша выбрана справа, сначала идите налево.

3.4 реализация кода версии hoare

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);

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

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
 
 

Тест кода версии 3.5 hoare

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
 //快速排序递归实现
 //快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边找大
		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 = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

4. Метод копания

4.1 Идеи реализации

Указываем сортировать по возрастанию , имя сортируемого массива a, ключ значения ссылки, лево лево, право право.

1. Выберите ключ. Ключом может быть любой элемент в массиве, который нужно отсортировать. Мы по-прежнему выбираем ключ как a[left] и присваиваем ключу a[left] для сохранения. Позиция выбранного элемента в качестве ключа является положение ямы ;

2. Идти влево от правого конца (хвоста) массива, когда a[right] < клавиша, right останавливается, ставим a[right] в ямку, а pit меняется на позицию с индексом right, при этом правильное время Не двигаться;

3. Затем идем вправо от левого конца (головы) массива, когда клавиша a[left] >, левая останавливается, ставим a[left] в ямку, а ямку меняем на позицию с нижним индексом left , в это время левая не двигается;

4. Повторите шаги 2 и 3. Когда левый и правый переходят в одно и то же положение, это положение является последним положением ямы.Поместите ключ в это положение ямы, и окончательное положение ключа будет определено, и нет необходимости чтобы отрегулировать его позже.

5. В это время ключ делит массив на левый и правый интервалы.Используем первые 4 шага для левого и правого интервалов, а затем снова определяем ключи левого и правого интервалов, рекурсируем левый интервал, а затем повторите правильный интервал для достижения сортировки.

Отличие копательного метода от хорового варианта:

1. Метод копания ямы не должен думать, что элементы в конечном положении встречи L и R меньше ключа, потому что конечное положение встречи - это положение ямы, просто поместите ключ непосредственно в положение ямы;

2. Не нужно думать о том, почему вы должны сначала идти направо, если вы выбираете левый ключ в качестве ключа, и сначала идти влево, если вы выбираете правый ключ в качестве ключа. потому что вам нужно заполнить отверстие слева, вы должны сначала пойти направо. Более уместно понимать это предложение, ища маленькое слева и большое справа.

4.2 Схема мышления

4.3 Кодовая реализация метода копания

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

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

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

4.4 Проверка кода метода копания

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

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

	int keyi = PartSort2(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

5. Версии с передним и задним указателем

5.1 Идеи реализации

Мы указываем сортировку в порядке возрастания , имя отсортированного массива — a, а ссылочное значение — ключ .

1. Выберите ключ, ключ может быть любым элементом в сортируемом массиве, мы по-прежнему выбираем ключ как [левый];

2. Определите указатель prev и указатель cur, инициализируйте prev так, чтобы он указывал на начальную позицию массива, а cur указывает на следующую позицию prev. Cur идет первым, cur находит элементы меньше ключа, останавливается после их нахождения, пропускает prev++, а затем меняет местами (a[cur], a[prev]). После обмена продолжаем возвращаться, значение которое находит cur не меньше ключа, cur продолжает возвращаться, найдя его, пускаем prev++, обмениваем (a[cur], a[prev]), и повторяем это шаг;

3. Когда cur пройдёт весь массив, меняем местами (a[left], a[prev]), после чего определяется конечная позиция ключа. key делит массив на левый и правый подинтервалы, левый подинтервал меньше ключа, а правый подинтервал больше ключа;

4. Левый и правый подинтервалы продолжают повторять первые 3 шага, а сортировка массива осуществляется рекурсивным спуском вниз.

5.2 Схема мышления

Непрерывный обмен здесь фактически бросает значение, меньшее, чем ключ вперед, и бросает значение, большее, чем ключ, назад.Значение между cur и prev на самом деле является всеми значениями, большими, чем ключ.Непрерывный обмен реализует окончательное деление по ключу Левый и правый интервалы, левый интервал меньше ключа, а правый интервал больше ключа.

5.3 Кодовая реализация метода переднего и заднего указателя

// 快速排序前后指针法
int PartSort3(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)
		{
			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 = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

5.4 Проверка кода до и после метода указателя

// 快速排序前后指针法
int PartSort3(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)
		{
			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 = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}
void test()
{
	int a[] = { 6,3,2,1,5,7,9 };
	QuickSort(&a, 0, sizeof(a) / sizeof(int) - 1);
	Print(&a, sizeof(a) / sizeof(int));
}
int main()
{
	test();

	return 0;
}

6. Анализ временной сложности

6.1 В лучшем случае

В приведенных выше трех случаях наилучшая временная сложность составляет O(N*logN) .

Каждый раз, когда ключ помещается в середину интервала, он должен рекурсировать logN раз, как двоичное дерево, а временная сложность сортировки каждого подинтервала составляет O (N), поэтому в лучшем случае это O (N * logN).

6.2 Наихудший случай

Когда массив отсортирован, временная сложность составляет O(N^2) независимо от того, является ли ключ крайним слева или самым правым .

7. Оптимизированная быстрая сортировка

Есть две идеи для быстрой оптимизации сортировки:

1. Мы можем оптимизировать метод выбора ключа;

2. Чтобы вернуться к небольшому подинтервалу, мы можем рассмотреть возможность использования сортировки вставками, также известной как межмалая оптимизация.

7.1 Оптимизация выбора ключей

Оптимизация выбора ключей в основном предназначена для упорядоченных или близких к упорядоченным массивов.

У нас есть две идеи по оптимизации выбора ключей:

1. Случайным образом выберите ключ;

2. Возьмите выбранный ключ на три цифры. (Возьмите левое, среднее, правое и выберите промежуточное значение среди чисел, нижние индексы которых являются этими тремя позициями в качестве ключа).

Первый способ мышления неуправляем, поэтому второй способ подбора ключей является наиболее подходящим.

Ниже приведен оптимизированный код для получения трех чисел:

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

После того, как мы его получим, мы меняем число на a[left], и нет проблем с тем, чтобы по-прежнему использовать предыдущий метод с прямыми и обратными указателями. Версия Холла — это тот же метод оптимизации, что и метод копания.

Если мы не оптимизируем выбор трех чисел, когда массив упорядочен или близок к упорядочению, временная сложность будет в худшем случае, O (N ^ 2). После взятия центра трех чисел, если массив упорядочен, временная сложность по-прежнему O (N * logN).

7.2 Межсотовая оптимизация

При рекурсии нетрудно увидеть на картинке, которую мы нарисовали раньше.Когда мы постоянно делим, мы будем делить все больше и больше позже.Когда объем данных особенно велик, потребление стека будет очень большим, что вызовет риск переполнения стека. Поэтому, когда деление достигает определенного уровня, мы больше не делим и напрямую выбираем сортировку вставками. При нормальных обстоятельствах, когда количество данных подинтервала равно 10, мы больше не используем рекурсию и используем сортировку вставками напрямую.

Код реализации:

// 插入排序
//时间复杂度(最坏):O(N^2) -- 逆序
//时间复杂度(最好):O(N) -- 顺序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[i + 1];
		
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}
// 快速排序前后指针法
//[left, right]
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}
void QuickSort(int* a, int left, int right)
{
	//子区间只有一个值,或者子区间不存在的时候递归结束
	if (left >= right)
		return;

	//小区间优化
	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);
	}

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

Эти два метода оптимизации имеют определенную степень улучшения с точки зрения времени и пространства , но суть быстрой сортировки не изменилась, а оптимизация — просто вишенка на торте исходной идеи.

Supongo que te gusta

Origin blog.csdn.net/Ljy_cx_21_4_3/article/details/131794152
Recomendado
Clasificación