8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)

常见排序算法

  • 排序的概念
    • 1.排序:
    • 2.稳定性:
    • 3.内部排序:
    • 4.外部排序:
  • 一.直接插入排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
  • 二.希尔排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
  • 三.选择排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
  • 四.堆排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
  • 五.冒泡排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
  • 六.快速排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
      • (1).方法1: hoare版本 -- 左右指针法
      • (2).方法2:挖坑法
      • (3).方法3:前后指针法
      • 4.优化
      • (1).三数取中法
      • (2).小区间优化法
    • *快速排序非递归代码
  • 七.归并排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
    • *归并排序非递归图解及代码
  • 八.计数排序
      • 1.原理
      • 2.图片解释
      • 3.C语言代码实现
  • 排序归纳与总结
    • 排序归类索引图

排序的概念

1.排序:

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

2.稳定性:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
对于稳定性的解释比较抽象,举个简单的例子,在一场考试中,可以提前交卷,比如A同学得85分,B同学也得85分,但是A同学比B同学先交卷子。最后的成绩单应该按照成绩排序,但是由于A同学比B同学交卷早,所以A在排序完以后应该还在B的前面。

3.内部排序:

数据元素全部放在内存中的排序。

4.外部排序:

数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第1张图片

一.直接插入排序

1.原理

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。 元素集合越接近有序,直接插入排序算法的时间效率越高。

2.图片解释

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第2张图片

3.C语言代码实现

void InsertSort(int* a, int n)
{
     
	// 多趟排序
	for (int i = 0; i < n - 1; ++i)
	{
     
		// 把tmp插入到数组的[0,end]有序区间中
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
     
			if (tmp < a[end])
			{
     
				a[end + 1] = a[end];
				--end;
			}
			else
			{
     
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

二.希尔排序

1.原理

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。当gap=1时,此时又是直接插入排序。

2.图片解释

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第3张图片

3.C语言代码实现

void ShellSort(int* a, int n)
{
     
	// gap > 1的时候,预排序
	// gap == 1的时候,直接插入排序  O(N)
	int gap = n;
	while (gap > 1)
	{
     
		gap = (gap / 3 + 1);
		for (int i = 0; i < n - gap; ++i)
		{
     
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
     
				if (tmp < a[end])
				{
     
					a[end + gap] = a[end];
					end -= gap;
				}
				else {
     
					break;
				}
			}

			a[end + gap] = tmp;
		}
		//printf("gap:%d->", gap);
		//PrintArray(a, n);
	}
}

三.选择排序

1.原理

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
^在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
^若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
^在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

2.图片解释

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第4张图片
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第5张图片

3.C语言代码实现

我们实现代码的时候,遍历一次,找出最大和最小,使时间复杂度稍微简单一点点

void Swap(int* p1, int* p2)
{
     
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void SelectSort(int* a, int n)
{
     
	int left = 0, right = n - 1;
	while (left < right)
	{
     
		// 选出最大的值和最小的值
		int minIndex = left, maxIndex = left;
		for (int i = left; i <= right; ++i)
		{
     
			if (a[i] < a[minIndex])
				minIndex = i;

			if (a[i] > a[maxIndex])
				maxIndex = i;
		}

		Swap(&a[left], &a[minIndex]);
		// 如果max和left位置重叠,max被换走了,要修正一下max的位置
		if (left == maxIndex)
		{
     
			maxIndex = minIndex;
		}

		Swap(&a[right], &a[maxIndex]);
		++left;
		--right;
	}
}

四.堆排序

1.原理

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

2.图片解释

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第6张图片

3.C语言代码实现

void Swap(int* p1, int* p2)
{
     
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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[parent], &a[child]);
			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);
	}

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

五.冒泡排序

1.原理

就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.图片解释

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第7张图片
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第8张图片

3.C语言代码实现

代码中我们引入exchange变量,当数组有序时,我们没有必要继续执行下去,加入exchange变量后可再数组有序情况下跳出,提高时间复杂度。

void Swap(int* p1, int* p2)
{
     
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void BubbleSort(int* a, int n)
{
     
	for (int end = n; end > 0; --end)
	{
     
		int exchange = 0;
		for (int i = 1; i < end; ++i)
		{
     
			if (a[i - 1] > a[i])
			{
     
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}

		if (exchange == 0)
		{
     
			break;
		}
	}

六.快速排序

1.原理

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2.图片解释

(1).方法1: hoare版本 – 左右指针法
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第9张图片

(2).方法2:挖坑法
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第10张图片

(3).方法3:前后指针法
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第11张图片

3.C语言代码实现

(1).方法1: 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;
}

(2).方法2:挖坑法

int PartSort2(int* a, int left, int right)
{
     
	int key = a[left];
	while (left < right)
	{
     
		// 找小
		while (left < right && a[right] >= key)
		{
     
			--right;
		}

		// 放到左边的坑位中,右边就形成新的坑
		a[left] = a[right];

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

		// 放到右边的坑位中,左边就形成新的坑
		a[right] = a[left];
	}

	a[left] = key;

	return left;
}

(3).方法3:前后指针法

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

		++cur;
	}

	Swap(&a[keyi], &a[prev]);

	return prev;
}

4.优化

(1).三数取中法

在快排的过程中,每一次我们都要去一个元素作为枢纽值,以这个数字来将序列划分为两个部分,我采用三数取中的方法来找这个枢纽值,也就是取左端,中间,右端三个数,然后进行排序,将中间数作为枢纽值。因对快速排序影响最大 的是选择枢纽值,如果枢纽值越接近中位数,划分区间越接近二分,效率越高。将以下代码插入每种方法函数内即可。

int GetMidIndex(int* a, int left, int right)
{
     
	int mid = (left + right) >> 1;
	// left  mid  right
	if (a[left] < a[mid])
	{
     
		if (a[mid] < a[right])
		{
     
			return mid;
		}
		else if (a[left] > a[right])
		{
     
			return left;
		}
		else
		{
     
			return right;
		}
	}
	else // a[left] > a[mid]
	{
     
		if (a[mid] > a[right])
		{
     
			return mid;
		}
		else if (a[left] < a[right])
		{
     
			return left;
		}
		else
		{
     
			return right;
		}
	}
}


int midIndex = GetMidIndex(a, left, right);
Swap(&a[left], &a[midIndex]);

(2).小区间优化法

这个优化方法是,减少递归树的最后几层,所以当这个子区间是数据较小,再去分治递归不太划算,我们选择其他排序方法来处理剩下的几个数,代码中使用的是插入排序,也可以使用其他的排序方法,但选择任何排序都是有消耗的。

void QuickSort(int* a, int begin, int end)
{
     
	if (begin >= end)
		return;

	// 1、如果这个子区间是数据较多,继续选key单趟,分割子区间分治递归
	// 2、如果这个子区间是数据较小,再去分治递归不太划算
	if (end - begin > 20)
	{
     
		int keyi = PartSort3(a, begin, end);

		// [begin, keyi-1] keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else
	{
     
		//HeapSort(a + begin, end - begin + 1);
		InsertSort(a + begin, end - begin + 1);
	}
}

*快速排序非递归代码

快速排序的非递归方法需要用到栈的增删查改,前几篇博客有提到过,由于C语言的栈使用很麻烦,我这里只引用函数,并且添加注释,详细请看“栈和队列的增删查改”此篇博客

// 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 QuickSortNonR(int* a, int begin, int end)
{
     
	Stack st;//定义一个栈
	StackInit(&st);//初始化栈
	StackPush(&st, begin);//将begin入栈
	StackPush(&st, end);//将end入栈

	while (!StackEmpty(&st))//判断栈是否为空
	{
     
		int left, right;
		right = StackTop(&st);//取栈顶元素end
		StackPop(&st);//删除栈顶元素end

		left = StackTop(&st);//取栈顶元素begin
		StackPop(&st);//删除栈顶元素begin

		int keyi = PartSort1(a, left, right);//用左右指针的方法对区间[begin end]进行排序
		if (left < keyi - 1)
		{
     
			StackPush(&st, left);//入栈left
			StackPush(&st, keyi - 1);//入栈keyi-1
		}

		if (keyi + 1 < right)
		{
     
			StackPush(&st, keyi + 1);//入栈keyi-1
			StackPush(&st, right);//入栈right
		}
	}

	StackDestroy(&st);//销毁栈
}

七.归并排序

1.原理

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

2.图片解释

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第12张图片

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第13张图片

3.C语言代码实现

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

	int mid = (left + right) >> 1;
	// [left, mid][mid+1,right]
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	// 两段有序子区间归并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);
}

*归并排序非递归图解及代码

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第14张图片

void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
     
	int j = begin1;
	int i = begin1;
	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 (; j <= end2; ++j)
		a[j] = tmp[j];
}


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, begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 如果第二个小区间不存在就不需要归并了,结束本次循环
			if (begin2 >= n)
				break;

			// 如果第二个小区间存在,但是第二个小区间不够gap个,结束位置越界了,需要修正一下
			if (end2 >= n)
				end2 = n - 1;

			_Merge(a, tmp, begin1, end1, begin2, end2);
		}

		gap *= 2;
	}

	free(tmp);
}

八.计数排序

1.原理

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
只适合,一组数据,数据的范围比较集中. 如果范围集中,效率是很高的,但是局限性也在这里并且只适合整数,如果是浮点数、字符串等等就不行了

2.图片解释


8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第15张图片

3.C语言代码实现

void CountSort(int* a, int n)
{
     
	int max = a[0], min = a[0];
	for (int i = 0; i < n; ++i)
	{
     
		if (a[i] > max)
			max = a[i];

		if (a[i] < min)
			min = a[i];
	}

	int range = max - min + 1;
	int* count = malloc(sizeof(int) * range);
	memset(count, 0, sizeof(int) * range);
	for (int i = 0; i < n; ++i)
	{
     
		count[a[i] - min]++;
	}

	int i = 0;
	for (int j = 0; j < range; ++j)
	{
     
		while (count[j]--)
		{
     
			a[i++] = j + min;
		}
	}

	free(count);
}

排序归纳与总结

时间复杂度,空间复杂度,稳定性表格
8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第16张图片

排序归类索引图

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)_第17张图片

你可能感兴趣的:(数据结构,排序,算法,数据结构,排序算法,快速排序)