内 部 排 序

欢迎访问我的博客首页


  常用排序算法有插入排序、选择排序、交换排序三类。

排序

  插入排序、交换排序(冒泡排序)都是稳定的,且有最好和最坏时间;选择排序不稳定且只有最坏时间。它们的衍生算法:希尔排序、堆排序、快速排序都不稳定。
  排序的操作有比较、移动、交换,这三个操作的耗时是递增的。。
  插入排序在元素基本有序时最优。因为选择排序没有最优时间;冒泡排序需要进行完整的一轮两两比较操作,同时有交换操作。插入排序只需要部分比较操作,然后进行部分移动操作,最后进行一次交换操作。
  快速排序使序列分段有序,减少移动和交换次数,所以快。
  快速排序在元素基本有序时最差。元素基本有序时,每次选取的基准元素接近最小值或最大值,快速排序退化成冒泡排序。

2. O(nlogn) 的排序算法

  堆排序不是稳定排序,但性能稳定且只需 O(1) 的额外空间。每轮排序能确定根结点位置。
  快速排序不是稳定排序且性能不稳定,需要 O(nlogn) 的额外空间。每轮排序能确定基准元素位置。
  归并排序是稳定排序且性能稳定,但需要 O(n) 的额外空间。每轮排序不能确定元素位置。

1. 插入排序


  插入排序包括直接插入排序和希尔排序。直接插入排序是稳定的,代码如下:

template<typename T>
void insertSort(T* a, size_t n) {
	for (int i = 1; i < n; i++) {
	 	if (a[i] < a[i-1]) {
	 		T temp = a[i];
	 		int j;
	 		// a[i]前面的元素后移1位。
	 		for (j = i - 1; j >=0 && a[j] > temp; j--)
	 			a[j + 1] = a[j];
	 		a[j + 1] = temp;
	 	}
	}
}

2. 选择排序


  选择排序包括直接选择排序和堆排序。两者是不稳定排序,但性能稳定:直接选择排序性能总是最差,堆排序总是 n l o g 2 n nlog_2n nlog2n

2.1 直接选择排序


// a[i]被交换引起不稳定。
template<typename T>
void selectSort(T* a, size_t n) {
	for (int i = 0; i < n - 1; i++) {
		int min = i;
		// a[i]前有序。从a[i]后面选择最小的元素放在a[i]。
		for (int j = i + 1; j < n; j++)
			if (a[j] < a[min])
				min = j;
		if (min != i)
			swap(a[i], a[min]);
	}
}

2.2 堆排序


  堆排序使用的堆是完全二叉树,所以可以用数组存储。

二叉树

  从上图中的 (a) 可以看出,假如从 0 开始编号:结点 x 的左右子结点的编号分别是 2x+1、2x+2;假如结点总数是 n,最后一个非叶子的编号是 n 2 − 1 \frac{n}{2}-1 2n1

堆排序

  堆排序。

void HeapAdjust(int* arr, int parent, int n) {
	int temp = arr[parent], child = 2 * parent + 1;
	while (child < n) {
		if (child + 1 < n && arr[child] < arr[child + 1])
			child++;
		if (temp > arr[child])
			break;
		arr[parent] = arr[child];
		parent = child;
		child = 2 * child + 1;
	}
	arr[parent] = temp;
}

void HeapSort(int* arr, int n) {
	for (int i = n / 2 - 1; i >= 0; i--)
		HeapAdjust(arr, i, n);
	for (int i = n - 1; i > 0; i--) {
		swap(arr[i], arr[0]);
		HeapAdjust(arr, 0, i);
	}
}

  上面实现的是大顶堆,用于从小到大排序。如果要实现小顶堆,把第 4 行的第 2 个小于号改为大于号,且把第 6 行的大于号改为小于号

3. 交换排序


  交换排序包括冒泡排序和快速排序。冒泡排序是稳定的,快速排序不稳定。

3.1 冒泡排序


template<typename T>
void bubbleSort(T* a, size_t n) {
	for (int i = n - 1; i > 0; i--)
		// a[i]后有序。
		for (int j = 0; j < i; j++)
			if (a[j] > a[j + 1])
				swap(a[j], a[j + 1]);
}

  冒泡排序每轮排好一个元素放在序列尾部,所以第一层循环是倒序的,第二层循环只排前部无序的部分。

3.2 快速排序


  快速排序的主要部分是 partition 函数。partition 函数的作用是确定基准元素的位置,且使基准元素前面的元素都比它小,后面的元素都比它大(从小到大排序)。

快速排序

  一般以待排序列第一个元素作为基准元素。比如要对 0 到 9 这 10 个元素从小到大排序且第一个元素是 5。partition 执行一次后元素 5 就被放在第 6 个位置,且它前面的 5 个数都比它小,它后面的 4 个数都比它大。可以使用任意算法让基准元素前面的元素都比它小,后面的元素都比它大,比如:

  1. 从两个方向:把基准元素后方比它小的元素移到它前方,同时把基准元素前方比它大的元素移到它后方。
  2. 从一个方向:从基准元素的一端选择比它大或者小的元素放在它另一端。

  下面把这两种算法都实现一下。我们先以第一个元素为基准元素实现两种算法,再以最后一个元素为基准元素实现两种算法。

1. 以第一个元素为基准元素

int partition_11(int* arr, int start, int end) {
	int pivotkey = arr[start];
	while (start < end) {
		while (start < end && arr[end] >= pivotkey)
			end--;
		arr[start] = arr[end];
		while (start < end && arr[start] <= pivotkey)
			start++;
		arr[end] = arr[start];
	}
	arr[start] = pivotkey;
	return start;
}
int partition_21(int* arr, int start, int end) {
	int pivotidx = end + 1;
	for (int i = end; i > start; i--) {
		if (arr[i] > arr[start]) {
			pivotidx--;
			if (pivotidx != i)
				swap(arr[pivotidx], arr[i]);
		}
	}
	pivotidx--;
	swap(arr[pivotidx], arr[start]);
	return pivotidx;
}

2. 以最后一个元素为基准元素

int partition_12(int* arr, int start, int end) {
	int pivotkey = arr[end];
	while (start < end) {
		while (start < end && arr[start] <= pivotkey)
			start++;
		arr[end] = arr[start];
		while (start < end && arr[end] >= pivotkey)
			end--;
		arr[start] = arr[end];
	}
	arr[start] = pivotkey;
	return start;
}
int partition_22(int* arr, int start, int end) {
	int pivotidx = start - 1;
	for (int i = start; i < end; i++) {
		if (arr[i] < arr[end]) {
			pivotidx++;	
			if (pivotidx != i)
				swap(arr[pivotidx], arr[i]);
		}
	}
	pivotidx++;
	swap(arr[pivotidx], arr[end]);
	return pivotidx;
}

  注意:partition 函数中 arr[start]、arr[end] 和 pivotkey 比较时至少有一个要带等号,而不能都用大于或小于。否则当 arr[start] 等于 are[end] 时三者相等,partition 函数的第一个 while 循环会进入死循环。

3. 调用 partition 的递归算法与非递归算法

void QuickSort(int* arr, int start, int end) {
	if (start >= end)
		return;
	int pivotloc = partition(arr, start, end);
	QuickSort(arr, start, pivotloc - 1);
	QuickSort(arr, pivotloc + 1, end);
}

void QuickSort(int* arr, int start, int end) {
	stack<int> st;
	st.push(start);
	st.push(end);
	while (st.empty() != true) {
		int end = st.top();
		st.pop();
		int start = st.top();
		st.pop();

		int pivotloc = partition(arr, start, end);
		if (pivotloc - 1 > start) {
			st.push(start);
			st.push(pivotloc - 1);
		}
		if (pivotloc + 1 < end) {
			st.push(pivotloc + 1);
			st.push(end);
		}
	}
}

4. 归并排序


  下面是使用归并排序从小到大排序的示意图。

归并排序
图  3 归并排序 图 \ 3 \quad 归并排序  3归并排序

  图 3 是归并排序示意图。归并排序是一个先拆分后合并的过程。拆分的过程对数组划分,合并的过程把两个无序数组合并成一个有序数组。下面先给出递归形式的归并排序,再给出非递归形式的归并排序。

void Merge(int* arr, int* temp, int start, int middle, int end) {
	int p_left = start, p_right = middle + 1, p_temp = 0;
	while (p_left <= middle && p_right <= end) {
		if (arr[p_left] <= arr[p_right])
			temp[p_temp++] = arr[p_left++];
		else
			temp[p_temp++] = arr[p_right++];
	}
	while (p_left <= middle)
		temp[p_temp++] = arr[p_left++];
	while (p_right <= end)
		temp[p_temp++] = arr[p_right++];
	memcpy(arr + start, temp, sizeof(int) * (end - start + 1));
}

void MSort(int* arr, int* temp, int start, int end) {
	if (start >= end)
		return;
	int middle = (start + end) / 2;
	MSort(arr, temp, start, middle);
	MSort(arr, temp, middle + 1, end);
	Merge(arr, temp, start, middle, end);
}

void MergeSort(int* arr, int n) {
	int* temp = new int[n];
	MSort(arr, temp, 0, n - 1);
	delete[] temp;
}

  代码:主要部分是 Merge 函数,该函数的作用是把两个有序数组 arr[start : middle] 和 arr[middle+1 : end] 合并成有序数组 temp[0 : end-start+1],再把 temp[0 : end-start+1] 拷贝到 arr[start : end]。所以归并排序需要一个和待排序列同样大小的额外空间。下面是非递归实现:

void MergeSort(int* arr, int n) {
	int* temp = new int[n];
	for (int seg = 1; seg < n; seg *= 2)
		for (int start = 0; start < n; start += seg * 2) {
			start = min(start, n - 1);
			int middle = min(start + seg - 1, n - 1), end = min(start + seg * 2 - 1, n - 1);
			Merge(arr, temp, start, middle, end);
		}
	delete[] temp;
}

  非递归算法依然调用 Merge 函数。在 Merge 函数的第 2 行,middle 被划分到左侧,所以非递归算法的第 6 行为了保存一致,也都把 middle 划分到左侧。

5. 临时


  1. 选择排序:每轮确定一个前部元素。比较次数 = n-1 + n-2 + … + 1。最小交换次数 = 0,最大交换次数 = n-1。
    因为比较次数固定,所以时间恒为 O(n^2)。因为交换,所以不稳定。
  2. 插入排序:每轮确定一个前部元素。最小比较次数 = n-1,最大比较次数 = 1 + 2 + … + n-1。最小移动次数 = 0,最大移动次数 = 1 + 2 + … + n-1。
    时间不恒定。因为移动,所以稳定。
  3. 冒泡排序:每轮确定一个后部元素。最小比较次数 = n-1,最大比较次数 = n-1 + n-2 + … + 1。最小交换次数 = 0,最大交换次数 = n-1 + n-2 + … + 1。
    时间不恒定。因为是相邻交换,所以稳定。
  4. 快速排序:每轮确定一个中轴元素。最小比较次数 = n-1,最大比较次数 = n-1 + 2((n-1)/2) + 4

6. 参考


  1. 内部排序
  2. 快速排序
  3. 归并排序-菜鸟教程
  4. 各种排序的动图
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值