常用排序算法
常用排序算法(图解)
作为一个程序猿,在学习完语言(c,java,python,……)之后,就会接触算法,而算法可大致分为基本算法、加密算法、排序算法、检索算法等等算法。大家接触的第一类算法就是我今天要说的“排序算法”。所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。不稳定排序算法可以被特别地时作为稳定。作这件事情的一个方式是人工扩充键值的比较,如此在其他方面相同键值的两个物件间之比较,就会被决定使用在原先资料次序中的条目,当作一个同分决赛。然而,要记住这种次序通常牵涉到额外的空间负担。
1、排序算法概述
1.1 排序算法分类
常见排序算法可以分为两大类:
非线性时间比较类排序: 通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等
线性时间非比较类排序: 不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。 主要有:计数排序,基数排序,桶排序等。
1.2 排序复杂度
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 | 备注 |
---|---|---|---|---|---|---|
插入排序 | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O(n) | O(1) | 稳定 | |
希尔排序 | O(n log 2 n \log_2 n log2n)~O( n 2 n^2 n2) | O( n 2 n^2 n2) | O( n 1.3 n^{1.3} n1.3) | O(1) | 不稳定 | |
冒泡排序 | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O(n) | O(1) | 稳定 | |
快速排序 | O(n log 2 n \log_2 n log2n) | O( n 2 n^2 n2) | O(n log 2 n \log_2 n log2n) | O(n log 2 n \log_2 n log2n) | 不稳定 | |
选择排序 | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O( n 2 n^2 n2) | O(1) | 不稳定 | |
堆排序 | O(n log 2 n \log_2 n log2n) | O(n log 2 n \log_2 n log2n) | O(n log 2 n \log_2 n log2n) | O(1) | 不稳定 | |
归并排序 | O(n log 2 n \log_2 n log2n) | O(n log 2 n \log_2 n log2n) | O(n log 2 n \log_2 n log2n) | O(n) | 稳定 | |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 | |
基数排序 | O(n+k) | O( n 2 n^2 n2) | O(n) | O(n+k) | 稳定 | |
桶排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
1.3 相关概念
**稳定:**如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
**不稳定:**如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
**时间复杂度:**对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
**空间复杂度:**是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
2、插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
2.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
2.2 动图演示
2.3 代码实现
/**
* 直接插入排序
* @param arr 需要排序的数组
*/
public static void insertionSort(int[] arr) {
for(int i = 0; i < arr.length; i ++) {
int get = arr[i];
int j = i - 1;
while(j >= 0 && arr[j] > get) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = get;
}
}
3、二分法插入排序(Binary Insert Sort)
二分法插入排序又叫折半插入排序,对半插入排序,二分插入排序,二分法没有排序,只有查找。所以当找到要插入的位置时。移动必须从最后一个记录开始,向后移动一位,再移动倒数第2位,直到要插入的位置的记录移后一位。
3.1 算法描述
- 在插入第i个元素时,对前面的0~i-1元素进行折半,先跟他们中间的那个元素比,
- 如果小,则对前半再进行折半,否则对后半进行折半,直到left<right,
- 然后再把第i个元素前1位与目标位置之间的所有元素后移,再把第i个元素放在目标位置上。
3.2 演示图
3.3 代码实现
/**
* 二分插入排序
* @param arr 需要排序的数组
*/
public static void insertionSortBinary(int[] arr) { // 已抓牌为例
for (int i = 1; i < arr.length; i++) {
int get = arr[i]; // 右手抓到一张扑克牌
int left = 0; // 拿在左手上的牌总是排序好的,所以可以用二分法
int right = i - 1; // 手牌左右边界进行初始化
while (left <= right){ // 采用二分法定位新牌的位置
int mid = (left + right) / 2;
if (arr[mid] > get) {
right = mid - 1;
}else {
left = mid + 1;
}
}
for (int j = i - 1; j >= left; j--) { // 将欲插入新牌位置右边的牌整体向右移动一个单位
arr[j + 1] = arr[j];
}
arr[left] = get; // 将抓到的牌插入手牌
}
}
4、希尔排序(Shell Sort)
希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序是不稳定的排序算法。
4.1 算法描述
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 动图演示
4.3 代码实现
/**
* 希尔排序
* @param arr 需要排序的数组
*/
public static void shellSort(int arr[]) {
int h = 0;
while (h <= arr.length) { // 生成初始增量
h = 3 * h + 1; // 动态定义间隔序列
}
while (h >= 1) {
for (int i = h; i < arr.length; i++) {
int j = i - h;
int get = arr[i];
while (j >= 0 && arr[j] > get) {
arr[j + h] = arr[j];
j = j - h;
}
arr[j + h] = get;
}
h = (h - 1) / 3; // 递减增量
}
}
5、冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
5.1 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较,排序完成。
5.2 动图演示
5.3 代码实现
/**
* 冒泡排序 Bubble Sort
* @param arr 处理的数组
*/
public static void bubbleSort(int[] arr) {
for(int i = 0; i < arr.length - 1; i ++) {
for(int j = 0; j < arr.length-1-i; j ++) {
if(arr[j]<arr[j+1]) {// 从大到小 用<, 从小到大 用>
int temp = 0;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
6、鸡尾酒排序(Cocktail Sort)
鸡尾酒排序,也叫定向冒泡排序,鸡尾酒搅拌排序,搅拌排序(也可以视作选择排序的一种变形),涟漪排序,来回排序或快乐小时排序,是冒泡排序的一种变形。此算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。
6.1 算法描述
鸡尾酒排序等于是冒泡排序的轻微变形。不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。
以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问一次序列就可以完成排序,但如果使用冒泡排序则需要四次。但是在乱数序列的状态下,鸡尾酒排序与冒泡排序的效率都很差劲。
6.2 动图演示
6.3 代码实现
/**
* 鸡尾酒排序
* @param arr
*/
public static void cocktailSort(int arr[]) {
int len = arr.length;
int i, left = 0, right = len - 1;
int temp;
while (left < right) {
for (i = left; i < right; i++)
if (arr[i] > arr[i + 1]) {
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
right--;
for (i = right; i > left; i--)
if (arr[i - 1] > arr[i]) {
temp = arr[i];
arr[i] = arr[i - 1];
arr[i - 1] = temp;
}
left++;
}
}
7、快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
7.1 算法描述
- 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
- 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
- 从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
- 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
- 重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
7.2 动图演示
7.3 代码实现
/**
* 快速排序
*
* @param arr
* 需要排序的数组
* @param _left
* 低位
* @param _right
* 高位
*/
public static void quickSort(int arr[], int _left, int _right) {
int left = _left;
int right = _right;
int temp = 0;
if (left <= right) { // 待排序的元素至少有两个的情况
temp = arr[left]; // 待排序的第一个元素作为基准元素
while (left != right) { // 从左右两边交替扫描,直到left = right
while (right > left && arr[right] >= temp)
right--; // 从右往左扫描,找到第一个比基准元素小的元素
arr[left] = arr[right]; // 找到这种元素arr[right]后与arr[left]交换
while (left < right && arr[left] <= temp)
left++; // 从左往右扫描,找到第一个比基准元素大的元素
arr[right] = arr[left]; // 找到这种元素arr[left]后,与arr[right]交换
}
arr[right] = temp; // 基准元素归位
quickSort(arr, _left, left - 1); // 对基准元素左边的元素进行递归排序
quickSort(arr, right + 1, _right); // 对基准元素右边的进行递归排序
}
}
8、选择排序(Selection Sort)
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。。
8.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
8.2 动图演示
8.3 代码实现
/**
* 选择排序
* @param args
*/
public static void selectSort(int[] arr) {
int minIndex = 0;
int temp = 0;
if ((arr == null) || (arr.length == 0))
return;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;// 无序区的最小数据数组下标
for (int j = i + 1; j < arr.length; j++) {
// 在无序区中找到最小数据并保存其数组下标
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将最小元素放到本次循环的前端
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
9、堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
9.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
9.2 动图演示
9.3 代码实现
/**
* 堆排序
*
* @param arr
* 需要排序的数组
*/
public static void heapSort(int[] arr) {
// 1.构建大顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
// 从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr, i, arr.length);
}
// 2.调整堆结构+交换堆顶元素与末尾元素
for (int j = arr.length - 1; j > 0; j--) {
swap(arr, 0, j);// 将堆顶元素与末尾元素进行交换
adjustHeap(arr, 0, j);// 重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
*
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];// 先取出当前元素i
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {// 从i结点的左子结点开始,也就是2i+1处开始
if (k + 1 < length && arr[k] < arr[k + 1]) {// 如果左子结点小于右子结点,k指向右子结点
k++;
}
if (arr[k] > temp) {// 如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp;// 将temp值放到最终的位置
}
/**
* 交换位置
*
* @param arr
* 需要交换位置的 数组
* @param i
* 需要交换位置的 数组的 第一个值的下标
* @param j
* 需要交换位置的 数组的 第二个值的下标
*/
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
10、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2路归并。
10.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
10.2 动图演示
10.3 代码实现
/**
* 归并排序
* @param arr
*/
public static void mergeSort(int []arr){
int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
sortMerge(arr,0,arr.length-1,temp);
}
private static void sortMerge(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sortMerge(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sortMerge(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
11、计数排序(Counting Sort)
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
11.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
11.2 动图演示
11.3 代码实现
/**
* 计数排序
* @param arr 需要排序的数组
* @param k 数组中的 最大值
*/
public static void countingSort(int[] arr, int maxValue) {
int[] C = new int[maxValue + 1];// 构造C数组
int length = arr.length, sum = 0;// 获取A数组大小用于构造B数组
int[] B = new int[length];// 构造B数组
for (int i = 0; i < length; i++) {
C[arr[i]] += 1;// 统计A中各元素个数,存入C数组
}
for (int i = 0; i < maxValue + 1; i++) {// 修改C数组
sum += C[i];
C[i] = sum;
}
for (int i = length - 1; i >= 0; i--) {// 遍历A数组,构造B数组
B[C[arr[i]] - 1] = arr[i];// 将A中该元素放到排序后数组B中指定的位置
C[arr[i]]--;// 将C中该元素-1,方便存放下一个同样大小的元素
}
}
12、桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
12.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
12.2 演示图
12.3 代码实现
/**
* 桶排序
* @param arr 需要排序的数组
* @param bucketCount 桶大小
*/
public static void bucketSort(Integer arr[], int bucketCount) {
int len = arr.length;
double[] result = new double[len];
double min = arr[0];
double max = arr[0];
// 找到最大值和最小值
for (int i = 1; i < len; i++) {
min = min <= arr[i] ? min : arr[i];
max = max >= arr[i] ? max : arr[i];
}
// 求出每一个桶的数值范围
double space = (max - min + 1) / bucketCount;
// 先创建好每一个桶的空间,这里使用了泛型数组
ArrayList<Integer>[] arrList = new ArrayList[bucketCount];
// 把arr中的数均匀的的分布到[0,1)上,每个桶是一个list,存放落在此桶上的元素
for (int i = 0; i < len; i++) {
int index = (int) Math.floor((arr[i] - min) / space);
if (arrList[index] == null) {
// 如果链表里没有东西
arrList[index] = new ArrayList<Integer>();
arrList[index].add(arr[i]);
} else {
// 排序
int k = arrList[index].size() - 1;
while (k >= 0 && (Integer) arrList[index].get(k) > arr[i]) {
if (k + 1 > arrList[index].size() - 1) {
arrList[index].add(arrList[index].get(k));
} else {
arrList[index].set(k + 1, arrList[index].get(k));
}
k--;
}
if (k + 1 > arrList[index].size() - 1) {
arrList[index].add(arr[i]);
} else {
arrList[index].set(k + 1, arr[i]);
}
}
}
// 把各个桶的排序结果合并 ,count是当前的数组下标
int count = 0;
for (int i = 0; i < bucketCount; i++) {
if (null != arrList[i] && arrList[i].size() > 0) {
Iterator<Integer> iter = arrList[i].iterator();
while (iter.hasNext()) {
Integer d = (Integer) iter.next();
result[count] = d;
count++;
}
}
}
}
13、基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
13.1 算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
13.2 动图演示
13.3 代码实现
/**
* 基数排序
* @param array 待排序数组
* @param radix 基数(10,盒子个数)
* @param distanced 待排序中,最大的位数
* */
private static void radixSort(int[] array,int radix, int distance) {
int length = array.length;
int[] temp = new int[length];//用于暂存元素
int[] count = new int[radix];//用于计数排序 盒子 每一位的个
int divide = 1;
for (int i = 0; i < distance; i++) {
System.arraycopy(array, 0,temp, 0, length);
Arrays.fill(count, 0);//盒子清空
for (int j = 0; j < length; j++) {//这个循环用来把每个数的个十百千位分开,并且使相对应号数的桶的个数增加1
//divide : 1 10 100
//radix : 基数 10
int tempKey = (temp[j]/divide) % radix; //temp[j]/divide 每一位的个
count[tempKey]++; //每一位的个
}
//radix : 基数 10
for (int j = 1; j < radix; j++) {
count [j] = count[j] + count[j-1];
}
//个人觉的运用 数排序实现计数排序的重点在下面这个方法
for (int j = length - 1; j >= 0; j--) {
int tempKey = (temp[j]/divide)%radix;
count[tempKey]--;
array[count[tempKey]] = temp[j];
}
divide = divide * radix; // 1 10 100
}
}