算法笔记 排序算法完整介绍及C++代码实现 HERODING的算法之路

排序算法完整介绍及C++代码实现

  • 前言
  • 一、插入排序
    • 1.1 直接插入排序
    • 1.2 折半插入排序
    • 1.3 希尔排序
  • 二、交换排序
    • 2.1 冒泡排序
    • 2.2 快速排序
  • 三、选择排序
    • 3.1 简单选择排序
    • 3.2 堆排序
  • 总结

前言

数据结构刚好复习到排序部分,排序主要分为三种类型,插入排序、交换排序、选择排序,为了更好理解和记忆,这里我将代码和记录下来,以备遗忘之用,亦可为后人参考~


一、插入排序

插入排序的思想在于插入,即把要插入的数提取出来,找到要插入的位置,然后让该位置之后的数往后移动,可以理解为排队的时候,你让前k个人都往后移动一个位置,你站到空出来的位置上。主要类型有直接插入排序,折半插入排序,希尔排序,下面我将一一介绍。

1.1 直接插入排序

直接插入排序是一个稳定的,时间复杂度O(n2),空间复杂度O(1)(辅助空间)的排序算法,是一种简单直观的排序算法,整体思路就是往排序好的数组插入当前遍历的数字,假设待排序表为L,且L[0]位置为空,实现步骤如下:

  1. 查找出L(i) 在L[1 … n - 1]的插入位置k。
  2. 将L[k … n - 1]中的所有元素往后移动一位。
  3. 将L(i)复制到L[k]。

是不是很简单?尝试一波代码吧,代码如下:

#include
#include

using namespace std; 

class Solution {
public:
	void insertSort(vector<int>& A) {
		int len = A.size();
		int i, j, temp;
		for(int i = 1; i < len; i ++) {
			if(A[i] < A[i - 1]) {
				temp = A[i];
				for(j = i - 1; A[j] > temp && j >= 0; j --) {
					A[j + 1] = A[j];
				} 
				A[j + 1] = temp;
			}
		}
	} 	
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.insertSort(A);
	s.print(A);
	return 0;
} 

标准的函数实现形式,还可以在main函数中进行测试,当然,你可能会困惑为什么这里使用temp存储当前遍历的值,而不是用第一个位置进行存储,因为那只是一个思想,如果在真实使用情况下,不是每个数组的第一个位置都是空的(应该说几乎没有),希望你读完代码之后会对直接插入算法有一个更深刻的印象,OK,让我们继续吧。

1.2 折半插入排序

折半插入排序是一个稳定的,时间复杂度O(n2),空间复杂度O(1)(辅助空间)的排序算法,是在插入排序的基础上进行进一步的优化,当然思想还是没有变化的,只是在查找插入位置的时候使用了折半的思想,也就是二分法的思想,代码如下:

#include
#include

using namespace std; 

class Solution {
public:
	void halfInsertSort(vector<int>& A) {
		int len = A.size();
		int i, j, low, high, temp;
		for(int i = 1; i < len; i ++) {
			low = 0, high = i - 1, temp = A[i];
			while(low <= high) {
				int mid = (low + high) / 2;
				if(A[mid] > temp) {
					high = mid - 1;
				} else low = mid + 1;
			}
			for(j = i - 1; j >= high + 1; j --) {
				A[j + 1] = A[j];
			} 
			A[high + 1] = temp;
		}
	} 	
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.halfInsertSort(A);
	s.print(A);
	return 0;
} 

整体步骤还是很巧妙的,当然并不能从很大程度上对直接插入进行优化,但对数据量不是很大的排序表来说,它往往可以表现出很好的性能。

1.3 希尔排序

希尔排序利用了巧妙的思想,可以在一些情况下降低复杂度,空间复杂度为O(1),时间复杂度为O(n1.3)至O(n2),这是一套不稳定的排序方法,但是无伤大雅。大致思想在于分割,每次将数组按照步长进行分割,形成形成 len / step 组子模块,在各自的子模块中进行直接插入排序,然后不断缩小步长范围,代码如下:

#include
#include

using namespace std; 

class Solution {
public:
	void shellSort(vector<int>& A) {
		int len = A.size();
		int step, i, j, temp;
		for(step = len / 2; step >= 1; step /= 2) {
			for(i = step; i < len; i ++) {
				if(A[i] < A[i - step]) {
					temp = A[i];
					for(j = i - step; j >= 0 && A[j] > temp; j -= step) {
						A[j + step] = A[j]; 
					}
					A[j + step] = temp;
				}
			}
		}
	} 	
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.shellSort(A);
	s.print(A);
	return 0;
} 

听起来很复杂但是实现起来还是很容易的,主要是这个方法的思路实在是太巧妙了!让整个序列慢慢变有序而不是一部分直接有序另一部分直接无序,给人一种豁然开朗的感觉,好了,直接插入算法部分也就介绍完了,接下来就是交换排序部分!让我们开始吧!

二、交换排序

交换排序思想在于交换,即遇到不满足排序规则的两个数就要进行一个交换,主要类型有冒泡排序和快速排序,这是两个特别经典常用的算法,前者在于简单易于理解,后者在于时间复杂度化简到了O(nlogn),而且也很简单,下面我将一一介绍。

2.1 冒泡排序

冒泡排序,顾名思义,就像泡泡一样,每次遍历都把最大的数冒上去(即放到数组的后面),空间复杂度为O(1),时间复杂度为O(n2),这也是一种稳定的算法,代码如下:

#include
#include

using namespace std; 

// 交换函数 
void swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

class Solution {
public:
	void bubbleSort(vector<int>& A) {
		int i, j;
		int len = A.size();
		bool flag;
		for(int i = 0; i < len - 1; i ++) {
			flag = false;
			for(j = 1; j < len - i; j ++) {
				if(A[j] < A[j - 1]) {
					swap(A[j], A[j - 1]);
					flag = true;
				}
			}
			// 提前跳出,避免重复
			if(! flag) {
				break;
			}
		}
	} 	
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.bubbleSort(A);
	s.print(A);
	return 0;
} 

首先对于i,目的在于遍历的次数,而不代表位置,所以最大遍历次数为len - 1。其次可以看到对代码还是进行了一些简单的优化,第一点在于j的范围在于 1 —— len - i,因为遍历i 次后最后i个元素时有序的,不需要进行比较了,第二点在于设立了flag,如果当前遍历没有进行比较,那么说明数组已经是有序的了,直接跳出即可。

2.2 快速排序

快速排序算是算法入门级别较难的排序算法了,因为它应用了递归的方法,还有分治的思想,让它简洁且快速,空间复杂度为O(1),时间复杂度为O(nlogn),是一种不稳定的算法,但是在应用时没什么影响。整体思路是先定下一个基准(一般是最左的数),然后从左往右、从右往左找比该数大的和该数小的进行交换,直到碰头,碰头的位置放置该基准,这样基准左边的数都小于该数,右边的都大于该数,接在分别在左右区域各自进行同样操作(递归),代码如下:

#include
#include

using namespace std; 

class Solution {
public:
	void quickSort(vector<int>& A, int low, int high) {
		if(low < high) {
			int position = partition(A, low, high);
			quickSort(A, low, position - 1);
			quickSort(A, position + 1, high);
		}
	} 
	int partition(vector<int>& A, int low, int high) {
		int position = A[low];
		while(low < high) {
			while(low < high && A[high] >= position) -- high;
			A[low] = A[high];
			while(low < high && A[low] <= position) ++ low;
			A[high] = A[low];
		}
		A[low] = position;
		return low;
	}
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.quickSort(A, 0, A.size() - 1);
	s.print(A);
	return 0;
} 

可能需要注意的地方在交换那里,观察可以发现A[low] = A[high],这里直接赋值而没有用中间值存储,这是因为基准数已经用temp存储过了,所以覆盖的是基准值,之后的覆盖也都不会把已有的数抹去(因为前一个覆盖相当于复制总共两份,之后覆盖还剩一份),这也是为什么从右边开始的原因。好了,交换排序结束,选择排序正式开始!

三、选择排序

选择排序每次都是选择的是位置,选择最小的位置记录下来,思想上和冒泡很像,只不过是实现++细节上的不同,选择排序有简单选择排序和堆排序,下面我一一介绍。

3.1 简单选择排序

每次都找最小数的序号,然后与遍历的位置进行交换,可以理解成向前冒泡?代码如下:

#include
#include

using namespace std; 

// 交换函数 
void swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

class Solution {
public:
	void selectSort(vector<int>& A) {
		int len = A.size();
		int index, i, j;
		// 找最小的,放在前面
		for(i = 0; i < len - 1; i ++) {
			index = i;
			for(j = i + 1; j < len; j ++) {
				if(A[j] < A[index]) {
					index = j;
				}
			}
			if(index != i) {
				swap(A[index], A[i]);
			}
		}
	} 	
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.selectSort(A);
	s.print(A);
	return 0;
} 

没有什么技巧, 纯粹按照步骤去进行,所以也不过多讲解代码了,唯一注意和冒泡不同,这个是遍历一遍之后找到最小的位置才进行交换,认真看一遍就应该没问题。它是一种不稳定的算法,时间复杂度始终是O(n2)。接下来是重中之重,堆排序!

3.2 堆排序

堆排序实现方式是用一维数据的形式实现,但是思想上理解成完全二叉树的形式,堆的种类包括最小堆和最大堆,空间复杂度O(1),时间复杂度O(nlogn),效率是很高的,在实际操作中一般用优先队列实现。当然在堆中,我们只使用根部的节点数值,也就是说,整个一维数组并不是规规矩矩的从大到小或者从小到大排序的,能确定的只是根部节点是最大值或者最小值,这和堆的定义有关,n个关键字序列L[1…n]称为堆,当且仅当该序列满足:

  1. L(i) >= L(2i) 且 L(i) >= L(2i + 1) 或
  2. L(i) <= L(2i) 且 L(i) <= L(2i + 1) 1 <= i <= 向下取整的n/2。

这里以最大堆为例,整体步骤是先建立最大堆,然后调用最大堆函数实现堆排序。代码如下:

#include
#include

using namespace std;


// 交换函数 
void swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

class Solution {
public:
	void buildMaxHeap(vector<int>& A) {
		int len = A.size();
		for(int i = len / 2; i > 0; i --) {
			headAdjust(A, i, len);
		}
	} 	
	// 对位置 k 为根的子树进行调整 
	void headAdjust(vector<int>& A, int k, int len) {
		// 保存当前根位置的值 
		int temp = A[k];
		for(int i = k; i < len; i *= 2) {
			// 最大的子节点的值 
			if(i < len && A[i] < A[i + 1]) {
				i ++;
			}
			// 如果比最大子节点还大,不用比较了,直接跳过 
			if(A[i] < temp) {
				break;
			} else {// 如果小,子节点上去,修改k的值继续往下 
				A[k] = A[i];
				k = i;
			}
 		}
 		A[k] = temp;
	}
	
	void heapSort(vector<int>& A) {
		int len = A.size();
		buildMaxHeap(A);
		for(int i = len - 1; i > 0; i --) {
			swap(A[i], A[0]);
			headAdjust(A, 0, i - 1);
		}
	} 
	
	void print(vector<int>& A) {
		for(int i = 0; i < A.size(); i ++) {
			cout << A[i] << " ";
		}
	}
};

int main() {
	Solution s;
	int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1}; 
	vector<int> A(a, a + 9);
	s.heapSort(A);
	s.print(A);
	return 0;
} 

可以看到在堆排序中我用了不少注释来帮助理解,因为确实有亿点点复杂,其核心在于headAdjust上,那我逐步介绍,首先是初始化建立堆,建立堆的过程就要让整个数组符合堆排序的要求,从n/2位置开始向前进行,每到一个节点就需要调用一次headAdjust函数,使以该位置为根的树符合堆的要求,在headAdjust中,从当前(根)开始遍历子树,拿跟和子节点最大的进行比较,如果大,直接break,如果小了,把大的放上去,根拿下来,再接着往下比较。
那堆就不能排序了吗?当然能!只要把每次堆的头放入最后,就能够实现从小到大排序,这也是heapSort的过程。

总结

以上就是主要的排序算法的内容了,有简单的直接插入算法,简单交换算法,也有比较复杂的希尔排序,快排,堆排,但是万变不离其宗,只要理解了每个算法的具体思想,相信什么都不是问题,至于时间复杂度和空间复杂度,只要敲了一遍代码就能记在心中。 最后补充一下,排序还包括归并排序和基排序,这两个排序实现都很复杂,而且有各自更好的应用场景,所以重要的是理解思想,至于实现就不那么重要了。

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