博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
数据结构课程的内容基本忘光了,所以又拾起数据结构与算法的书本,重新学习了一边排序算法的原理和实现,在经过几个晚上的努力,终于把这几种排序算法给搞明白了,下面分享一下总结。注:以下博文中的动态图并非我画的,自行百度以下排序算法动态图就有一大堆了,很感谢那些画图的原作者前辈们,让我们理解起来更加简单,也更加形象。
接下来介绍一下排序算法的概念,下面直接引用百科资料:
【排序就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。】
数据结构之常用的排序算法列举:
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
基数排序堆排序
一张图看一下几种排序算法的时间复杂度与空间复杂度情况:
接下来,我们直接看每一种排序算法的原理以及代码实现,我使用的是 Java 语言编写的,本来是想用 C 语言写的,奈何大一学习的 C 如今早已抛掷脑后了,惭愧,惭愧。话不多说,我们直接看第一种排序,正文开始。
1、冒泡排序
冒泡排序是我们接触任何语言时一定会提及的一种排序算法,它是入门的必学算法之一。我在大一刚学 C 语言时,也时常被其困扰,为什么我徒手写不出一个冒泡排序出来,后面我才发现,关键还是没理解它的原理。
我现在发现,学习任何语言,或者说学习任何知识也好,首先你得搞清楚它的原理,这是至关重要的。那么我们来看看冒泡排序算法的实现原理:总结为一句话,比较相邻的两个元素,每次比较完后较大的一个数移动到本轮的末尾。
你可以想象一下这样,有一个小学生队伍需要进行从低到高排队,反正小学经常干这种事情。我通常与前面一位同学先比较,看看谁比较高,透露一个秘密,我小学时候好矮,经常排前面,坐第一排都是习惯就好。这里的冒泡法的对象是对于我而言,站在我的角度去与相邻的同学之间做比较。
冒泡排序的效果图:
冒泡排序的代码实现:
package com.xww;
import java.util.Arrays;
/**
* 冒泡排序(原理:比较相邻的两个元素,每次比较完毕最大的一个数移动到本轮的末尾。)
*
* @author xww
*
*/
public class BubbleSort {
// 无序数组
static int arr[] = new int[] { 2, 7, 6, 1, 8, 3, 5, 9, 6, -1, -4, 3 };
public static void main(String[] args) {
System.out.println(Arrays.toString(arr));
int len = arr.length;
// 循环n-1趟
for (int i = 0; i < len - 1; i++) {
// 每一趟移除已经排序过的,取剩下未排序的进行比较
for (int j = 0; j < len - 1 - i; j++) {
// 如果前面一个数比后面一个数大,交换两个数的位置
if (arr[j] > arr[j + 1]) {
arr[j] ^= arr[j + 1];
arr[j + 1] ^= arr[j];
arr[j] ^= arr[j + 1];
}
}
}
System.out.print(Arrays.toString(arr));
}
}
排序结果如下图:
在判断两个数的值大小是,冒泡排序采用的是交互两个数的位置,我上面采用的是一种二进制 ^ 计算,不用引入一个临时变量,直接对其进行交换。(下同)
2、选择排序
选择排序也是入门必定会接触的排序算法之一,和冒泡排序一样非常实用,可能会比冒泡理解起来困难一点点,因为它引入的是一个 index 变量来保存每一趟循环计算出的最大(小)的那个数的位置。它的实现原理:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
选择排序你可以想象为这样,还是以上面的排队为例子,老师看我们排队排的乱七八糟、参差不齐的,于是老师又进行了一遍排队操作。首先他以第一个小朋友为基准,与后面一个比较,发现第一个小朋友比较高,给他俩交换位置,又发现高了后面一个小朋友,又交换位置,直到给他拎到末尾去结束一轮排序。
第二轮从第二个小朋友开始,一直拎,直到全部拎完结束排序。哈哈哈哈,不要问为什么用拎,反正我小学老师在我们排队回家的时候,基本都是这样干的,小学一二年级非常小,才 8 岁这样,老师就跟夹娃娃一样。
选择排序的效果图:
选择排序的代码实现:
package com.xww;
import java.util.Arrays;
/**
* 选择排序(原理:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,
* 然后放到已排序序列的末尾。)
*
* @author xww
*
*/
public class SelectionSort {
// 无序数组
static int arr[] = new int[] { 2, 7, 5, -1, 8, 3, 1, 5, 9, 6, -3, 4 };
public static void main(String[] args) {
System.out.println(Arrays.toString(arr));
int len = arr.length;
int index;// 标记下标
for (int i = 0; i < len - 1; i++) {
// 标记前一个数
index = i;
// 与后面剩余的其他数依次做比较
for (int j = i + 1; j < len; j++) {
// 如果被标记的数比较大,则交换两数的位置
if (arr[j] < arr[index]) {
arr[j] ^= arr[index];
arr[index] ^= arr[j];
arr[j] ^= arr[index];
}
}
}
System.out.print(Arrays.toString(arr));
}
}
排序结果如下图:
3、 插入排序
插入排序呢,算是我们小时候就会的了,这个怎么说呢,比如我们小时候肯定都有玩过纸牌游戏吧(斗地主),我们取排的时候,按照我的习惯来说,我是比较喜欢把牌从小到大进行排列的,一个为了方便查找,不容易把牌给看漏掉,另一个就是为了装大手,别人出牌的那一刹那,我就把牌给扔下去了, 显得自己牌技高超,哈哈哈哈。
不过呢,与插入排序联系上的是,没当我们取一张牌的时候,可能是比较大,也可能比较小,这时候我们就要进行比较了,看看应该把牌插入到哪个位置,很显然这就用到了插入排序的算法,我靠,这不是人人都会嘛。
插入排序原理及算法步骤:
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,将该元素移到下一位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后 重复步骤2~5
插入排序的效果图:
插入排序的代码实现:
package com.xww;
import java.util.Arrays;
/**
* 插入排序(原理:插入排序的工作方式非常像人们排序一手扑克牌一样。)【步骤】1、从第一个元素开始,该元素可以认为已经被排序。
* 2、取出下一个元素,在已经排序的元素序列中从后向前扫描。 3、如果该元素(已排序)大于新元素,将该元素移到下一位置。
* 4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。 5、将新元素插入到该位置后 重复步骤2~5
*
* @author xww
*
*/
public class InsertionSort {
// 无序数组
static int arr[] = new int[] { 2, 7, -1, 8, 3, 1, 5, -4, 6, -3, 4 };
public static void main(String[] args) {
System.out.println(Arrays.toString(arr));
int len = arr.length;
for (int i = 1; i < len; i++) {
// 取得后一个数
int key = arr[i];
int j = i - 1;
// 与前一个做比较,如果大于后面的数,则把前一个数插入到后面的位置
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
// 如果前面一个数小于后面一个数,key就往后移动一位
arr[j + 1] = key;
}
System.out.print(Arrays.toString(arr));
}
}
排序结果如下图:
4、希尔排序
希尔排序有点不好理解吧,希尔排序是对插入排序的优化。为了减少数据的移动次数,在初始序列较大时取较大的步长,通常取序列长度的一半,此时只有两个元素比较,交换一次;之后步长依次减半直至步长为1,即为插入排序,由于此时序列已接近有序,故插入元素时数据移动的次数会相对较少,效率得到了提高。
它的实现原理:选择增量 gap=length/2,缩小增量继续以 gap = gap/2 的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。
希尔排序的效果图:
希尔排序的代码实现:
package com.xww;
import java.util.Arrays;
/**
* 希尔排序(原理:选择增量gap=length/2,缩小增量继续以gap = gap/2
* 的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。)
*
* @author xww
*
*/
public class ShellSort {
// 无序数组
static int arr[] = new int[] { -5, 8, 9, 1, 8, 7, 2, -3, 5, 4, -6, -1 };
public static void main(String[] args) {
System.out.println(Arrays.toString(arr));
int len = arr.length;
int temp;
int gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = arr[i];
int preIndex = i - gap;// 获取以 gap 为间隔的前一个值
while (preIndex >= 0 && arr[preIndex] > temp) {// 比较以 gap 为间隔的两个数,若前一个数比较大
arr[preIndex + gap] = arr[preIndex];// 将前一个数赋值给后一个数
preIndex -= gap;// 若 preIndex < 0 ,则 arr[] 将会溢出,此时结束循环体
}
arr[preIndex + gap] = temp; // 由于 preIndex -= gap 这里的 arr[preIndex + gap] 其实等于 arr[preIndex] 所以要将后面一个值赋值给 arr[preIndex] ,以作交换
}
gap /= 2;// 将间隔 gap 缩小一半,直到为 0
}
System.out.print(Arrays.toString(arr));
}
}
排序结果如下图:
5、归并排序
归并排序算法描述:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
它的算法实现步骤:
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个 最终的排序序列
归并排序的效果图:
归并排序的代码实现:
package com.xww;
import java.util.Arrays;
/**
* 归并排序(算法描述:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and
* Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;
* 即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
*
* 算法步骤:把长度为n的输入序列分成两个长度为n/2的子序列; 对这两个子序列分别采用归并排序; 将两个排序好的子序列合并成一个 最终的排序序列。)
*
* @author xww
*
*/
public class MergeSort {
// 无序数组
static int arr[] = new int[] { 2, 7, 6, 1, 8, 3, 5, 9, 6, -1, -4, 3 };
public static void main(String[] args) {
System.out.println(Arrays.toString(arr));
System.out.print(Arrays.toString(mergeSort(arr)));
}
private static int[] mergeSort(int arr[]) {
int len = arr.length;
if (len < 2)// 如果数组仅有一个元素,直接返回
return arr;
int mid = len / 2;
// 将数组分成两个子数组
int left[] = Arrays.copyOfRange(arr, 0, mid);
int right[] = Arrays.copyOfRange(arr, mid, len);
return merge(mergeSort(left), mergeSort(right));
}
private static int[] merge(int left[], int right[]) {
// 实例化一个新的数组,大小为给定数组的长度
int new_arr[] = new int[left.length + right.length];
int len = new_arr.length;
int i = 0;
int j = 0;
for (int index = 0; index < len; index++) {
if (i >= left.length)
new_arr[index] = right[j++];
else if (j >= right.length)
new_arr[index] = left[i++];
else if (left[i] > right[j])
new_arr[index] = right[j++];
else
new_arr[index] = left[i++];
}
return new_arr;
}
}
排序结果如下图:
6、快速排序
快速排序比较复杂,(类似于选择排序的定位思想)选一基准元素,依次将剩余元素中小于该基准元素的值放置其左侧,大于等于该基准元素的值放置其右侧。接着取基准元素的前半部分和后半部分分别进行同样的处理。以此类推,直至各子序列剩余一个元素时,即排序完成。
快速排序的实现方式,我这里写了两种方式:
(1)填坑法:
1、填坑法 (原理: 首先,选定基准元素 pivot,并标记其位置为 index,这个位置相当于一个“坑”。并且设置两个指针left和right,指向数列的最左和最右两个元素。
2、接着,从right指针开始,把指针所指向的元素和基准元素做比较。如果比pivot大,则right指针向左移动;如果比pivot小,则把right所指向的元素填入坑中。
3、再接着,我们切换到left指针进行比较。如果left指向的元素小于pivot,则left指针向右移动;如果元素大于pivot,则把left指向的元素填入坑中。
(2)指针交换法:
1、首先选定基准元素pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素。从right指针开始,把指针所指向的元素和基准元素做比较。
2、如果大于等于pivot,则指针向左移动;如果小于pivot,则right指针停止移动,切换到left指针。轮到left指针行动,把指针所指向的元素和基准元素做比较。
3、如果小于等于pivot,则指针向右移动;如果大于pivot,则left指针停止移动。这时,让left和right指向的元素进行交换。(由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。)
快速排序的效果图:
快速排序的代码实现:
package com.xww;
import java.util.Arrays;
/**
* 快速排序:一、填坑法 (原理: 首先,选定基准元素 pivot,并标记其位置为
* index,这个位置相当于一个“坑”。并且设置两个指针left和right, 指向数列的最左和最右两个元素。
* 接着,从right指针开始,把指针所指向的元素和基准元素做比较。如果比pivot大,则right指针向左移动;如果比pivot小,
* 则把right所指向的元素填入坑中。
* 再接着,我们切换到left指针进行比较。如果left指向的元素小于pivot,则left指针向右移动;如果元素大于pivot,
* 则把left指向的元素填入坑中。)
*
* 二、指针交换法(原理:首先选定基准元素pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素。从right指针开始,
* 把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向左移动;如果小于pivot,则right指针停止移动,切换到left指针。
* 轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向右移动;如果大于pivot,则left指针停止移动。
* 这时,让left和right指向的元素进行交换。 [由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。])
*
* @author xww
*
*/
public class QuickSort {
static int arr[] = new int[] { 6, 7, -6, 5, 3, 2, -2, 8, 4, 9, 7, 1 };
public static void main(String[] args) {
System.out.println(Arrays.toString(arr));
quickSortFill(arr, 0, arr.length - 1);
arr = new int[] { 6, 7, -6, 5, 3, 2, -2, 8, 4, 9, 7, 1 };
System.out.println("填坑法:" + Arrays.toString(arr));
quickSortPointer(arr, 0, arr.length - 1);
System.out.println("指针交换法:" + Arrays.toString(arr));
}
/**
* 填坑法,递归调用
*/
private static void quickSortFill(int arr[], int left, int right) {
// 递归结束条件:left大等于right的时候
if (left >= right) {
return;
}
int pivotIndex = fillSort(arr, left, right);
quickSortFill(arr, left, pivotIndex - 1);
quickSortFill(arr, pivotIndex + 1, right);
}
/**
* 指针交换法,递归调用
*/
private static void quickSortPointer(int arr[], int left, int right) {
if (left >= right) {
return;
}
// 得到基准元素位置
int pivotIndex = pointerSort(arr, left, right);
// 根据基准元素,分成两部分递归排序
quickSortPointer(arr, left, pivotIndex - 1);
quickSortPointer(arr, pivotIndex + 1, right);
}
/**
* 填坑法
*/
private static int fillSort(int arr[], int left, int right) {
// 取第一个位置的元素作为基准元素
int pivot = arr[left];
// 坑的位置,初始等于pivot的位置
int index = left;
while (right >= left) {
// right从右向左进行比较
while (right >= left) {
if (arr[right] < pivot) {
arr[index] = arr[right];
index = right;
left++;
break;
}
right--;
}
// left从左向右进行比较
while (right >= left) {
if (arr[left] > pivot) {
arr[index] = arr[left];
index = left;
right--;
break;
}
left++;
}
}
arr[index] = pivot;
return index;
}
/**
* 指针交换法
*/
private static int pointerSort(int arr[], int left, int right) {
int pivot = arr[left];
int startIndex = left;
// 如果left与right不相等,说明此趟排序未结束
while (left != right) {
// 控制right指针比较并左移
while (left < right && arr[right] > pivot) {
right--;
}
// 控制right指针比较并右移
while (left < right && arr[left] <= pivot) {
left++;
}
// 交换left和right指向的元素
if (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}
// pivot和指针重合点交换
int temp = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = temp;
return left;
}
}
排序结果如下图:
以上是几种常用的排序算法,还有堆排序、基数排序,本人还没写,以后再补。