1. 直接插入排序(Straight Insertion Sort)
- 基本要点:将一个数据插入到排序好的有序列表中,从而得到一个长度加1的的有序表。
- 直接插入实现原理:先将索引为0位置的值看成是一个有序的子序列,然后从第二个开始逐个进行插入操作,直到整个序列有序。
- 总结:将第一个数作为一个有序列表,然后第二个和第一个比较,即:每次都与最后一位进行比较,若大于,直接插在后面,若小于,往前一位继续比较,重复操作,直到大于以后,插入在后面或小于第一位后,放在第一位位置上,后面的数据依次后移。
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 其他插入方法:二分法,2-路插入法
- 代码实现:
/**
* 直接插入排序法
*/
public void straightInsertionSort(int[] array) {
// 从小到大排序
for (int i = 1; i < array.length; i++) {
// 若第i个元素大于i-1元素,直接插入(相当于位置不变)。
// 小于的话,移动有序表后插入(即依次交换位置)
if (array[i] < array[i - 1]) {
// 存储要交换位置的索引,即最后一个比他小的数的位置
int index = i - 1;
// 作为临时变量,存储要插入的数据(即待排序的数据)将a[i]位置空出来,用于移动
int temp = array[i];
// 将前一个数据后移,因为已经比前一个数据大了,所以直接移动一位
array[i] = array[i - 1];
// 依次前移比较,直到找到比temp大的数。获得第一个比他大的数的索引index
while (index != -1 && array[index] > temp) {
array[index + 1] = array[index];
// 元素后移,继续比较
index--;
}
// 插入到第一个比他大的数据后面->正确位置
array[index + 1] = temp;
}
}
}
2. 希尔排序(Shell’s Sort)
基本思路:先将整个待排序的记录序列分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”,再对全体记录进行依次直接插入排序。(若不进行最后这个操作,希尔排序算法会不稳定。=>最后的操作是将增量设置为1。)
注:很多算法解析中的增量解析都很难理解,这里做一下自我理解的增量解释,欢迎指正。
增量:每次子序列相隔的长度。增量越小,越稳定。即:分组时,是以{a[i],a[i+增量],a[i+增量+增量],a[i+增量+…+增量]}为一组进行比较的,直到数组a的下标超出长度。则不再循环。等于1时,是相邻的进行比较。
时间复杂度:没有确切的说法,不完全说法为O(n1.3)
空间复杂度:O(1)
代码示例:
/**
* 希尔排序(非稳定排序)
*
* @param a 排序的数组
*/
public void heerSort(int[] a) {
// 设置增量为数组的一半
int d = a.length / 2;
while (true) {
for (int i = 0; i < d; i++) {
// 第j个和第j+d进行比较
for (int j = i; j + d < a.length; j += d) {
// 进行位置交换
if (a[j] > a[j + d]) {
int temp = a[j];
a[j] = a[j + d];
a[j + d] = temp;
}
}
}
// 小于或等于1的增量,再继续运算没有意义 直接退出循环
if (d <= 1) {
break;
}
// 增量递减 比较范围缩短(越密集越准确)
//可以为其他的,例如:d-=2;但密集度不够,可能会导致排序不够稳定。
d--;
}
}
3. 简单选择排序(Simple Selection Sort)
基本思想:在要排序的一组数中,选出最小(或最大) 的一个数与第1个位置交换,然后在剩下的数当中再找最小(或最大)的数与第2个位置交换,依次类推,直到n-1(倒数第二个)元素和第n(最后一个元素)比较为止。
时间复杂度:O(n2)
空间复杂度:O(1)
代码实现:
/**
* 选择排序
*
* @param array
*/
public void selectSort(int[] array) {
// 每一个都进行遍历,和后面的进行比较。
for (int i = 0; i < array.length; i++) {
for (int j = i; j < array.length; j++) {
// 满足条件交换位置
if (array[i] < array[j]) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
}
4. 堆排序(Heap Sort)
基本思想:将数组下标构成一个大堆,即一维数组作为二叉树表示,再得到一个大堆。每构造一次大堆,该序列中的最大值就会沉淀(交换)到下标为0的地方。然后将下标为0的值和数组当前(每次交换后,大堆构造的长度-1)最后一位交换位置,继续进行大堆排序。直到序列有序。
即:利用二叉树的原理,将数组看作二叉树,进行大堆构造,将构造好的根节点(下标为0 的值)和最后一位交换,然后length-1,继续大堆构造。
大堆:根节点的值都比子节点的值大,则为大堆。
小堆:根节点的值都比子节点的值小,则为小堆。
时间复杂度:O(nlog2n)
空间复杂度:O(1)
代码示例:
/**
* 堆排序
*
* @param a
*/
public void heapSort(int[] a) {
// 构建大堆
buildMaxHeap(a);
for (int i = a.length - 1; i >= 1; i--) {
// 最大元素已经排在了下标为0的地方,把它放在最后面。循环执行,达到从小到大排序
exchangeElements(a, 0, i);// 每交换一次,就沉淀一个大元素
// 每次都从顶端开始构造,然后取出来放在该长度的最后面,最后长度-1。继续重复操作
maxHeap(a, i, 0);
}
}
/**
* 构建大堆
*
* @param a
*/
private void buildMaxHeap(int[] a) {
// 假设长度为9
// 只需要遍历一半 通过左右孩子分别等于2i+1 2i+2 可以直接得到他们的值,不用再进行遍历
int half = (a.length - 1) / 2;
for (int i = half; i >= 0; i--) {
// 若长度为9,只需遍历43210
maxHeap(a, a.length, i);
}
}
/**
*
* @param a 要遍历的数组
* @param length 表示用于构造大堆的数组的长度元素数量=>构造过以后,构造完的不再重复构造,即length-1
* @param i 表示从哪个节点遍历,由于大堆构造后会将值赋给最后一位,因此一般为0
*/
private void maxHeap(int[] a, int length, int i) {
// 左节点序号
int left = i * 2 + 1;
// 右节点序号
int right = i * 2 + 2;
// 最大的节点序号
int largest = i;
// 左边节点的比根节点的值大,将最大节点的序号改为left
if (left < length && a[left] > a[largest]) {
largest = left;
}
// 右边节点的比根节点的值大,将最大节点的序号改为left
if (right < length && a[right] > a[largest]) {
largest = right;
}
// 如果largest不等于i,则表示左右节点中有一个比它大
if (i != largest) {
// 进行数据交换
exchangeElements(a, i, largest);
// largest进行交换后,以他为根节点,对下面的左右孩子进行大堆构造,避免a[i]小于它下面的左右孩子
maxHeap(a, length, largest);
}
}
/**
* 在数组a里进行两个下标元素交换
*
* @param a 要交换的数组
* @param i 要交换的父节点
* @param largest 要交换的子节点
*/
private void exchangeElements(int[] a, int i, int largest) {
int temp = a[i];
a[i] = a[largest];
a[largest] = temp;
}
5. 冒泡排序(Bobble Sort)
基本思想:在要排序的一组数中,对当前还未排序的全部数,相邻的两个数依次进行比较和调整,让较大的数往上冒,较小的往下沉。即:每当两相邻的数比较后发现其排序与排序要求相反时,就将它们互换。
时间复杂度:O(n2)
空间复杂度:O(1)
代码示例:
1.传统冒泡法:一次找出一个最大值或最小值。
/**
* 冒泡排序
*
* @param a
*/
public void bubbleSort(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - 1 - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
2.双向冒泡法:一次找出一个最大值和最小值。然后两头同时缩减长度,让遍历次数减少。
/**
* 双向冒泡排序
*
* @param a
*/
public void bidirectionalBubbleSort(int[] a) {
// 即将放置最小数位置
int bottom = 0;
// 即将放置最大数位置
int top = a.length - 1;
// 交换时暂存数据
int tmp, j;
// 当放置最小值的位置大于等于放置最大值位置时,意味着两边的排序都完成了
while (bottom < top) {
// 正向冒泡,找到最大值
for (j = bottom; j < top; ++j)
if (a[j] > a[j + 1]) {
tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
// 修改即将放置最大数位置, 前移一位
top--;
// 反向冒泡,找到最小值
for (j = top; j > bottom; --j)
if (a[j] < a[j - 1]) {
tmp = a[j];
a[j] = a[j - 1];
a[j - 1] = tmp;
}
// 修改即将放置最小数位置,后移一位
bottom++;
}
}
6. 快速排序(Quick Sort)
基本思想:
- 选择一个基数(基准元素),通常为第一个元素或者最后一个元素。
- 通过一次排序后将待排序的记录分割成独立的两部分,其中一部分比基数大,一部分比基数小。(排序过程中,将比基数大和小的数位置做了调整)
- 此时基数元素在其排序后的正确位置。
- 对左右两部分进行同样方法的排序,直到整个序列有序。
时间复杂度:O(nlog2n)
空间复杂度:O(nlog2n)
代码示例:
/**
* 快速排序算法
*
* @param a
*/
public void quickSort(int[] a) {
if (a.length > 0) {
quick(a, 0, a.length - 1);
}
}
/**
* 进行递归
*
* @param a 排序的数组
* @param low 分段中最小的数的下标
* @param high 分段中最大的数的下标
*/
public void quick(int[] a, int low, int high) {
// 控制递归,避免死循环
if (low < high) {
int middle = getMiddle(a, low, high);
// 递归,继续将分段后的两端,分别排序
quick(a, 0, middle - 1);
quick(a, middle + 1, high);
}
}
/**
* 获取中间下标,即要插入的位置。也是分段的中间位置 同时已将基数插入到相应的位置。
*
* @param a 排序的数组
* @param low 分段中最小的数的下标
* @param high 分段中最大的数的下标
* @return 返回插入的位置,也是分段的中间位置
*/
private int getMiddle(int[] a, int low, int high) {
int temp = a[low];// 基数,基准元素
// 循环寻找位置,直到low>high找到基数应该插入的位置
while (low < high) {
while (low < high && a[high] >= temp) {
high--;
}
// 将右边的数丢给左边,左边开始计算
a[low] = a[high];
while (low < high && a[low] <= temp) {
low++;
}
// 将左边的数丢给右边,右边开始计算
a[high] = a[low];
}
// 插入到排序后正确的位置
a[low] = temp;
return low;
}
7. 归并排序(Merge Sort)
基本思想:将数组划分为小数组,最终划为一个个长度为1的数组,然后进行依次合并,每次合并后的子序列都是有序的。即:把待排序序列分为若干个长度为1的子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
时间复杂度:O(nlog2n)
空间复杂度:O(n)
大致实现机制:
- 将数组划分为一个个单位为1的子序列。(单位为1的子序列必然是有序的)
- 进行子序列的合并。
a.将要合并的两个子序列拿到。
b.将子序列进行比较:a[0]>b[0] 。
c.将小的那一个序列中的数添加到新数组中。例如 a[0]小于b[0]。newArray[0]=a[0]
d.将a数组下标右移,继续比较:a[1]>b[0]。依旧是小的那个添加到新数组中。然后将下标+1。
e.直到其中一方的下标到达其末尾为止。例如:二分时:前面的序列:到middle位置。后面的序列:到a.length-1位置。
f.将没有移动完的数组剩下的内容直接依次添加到新数组中。(一定比已添加的大,又因为是有序的,因此直接加在新数组后面即可。)
g.至此,合并操作完成。 - 递归合并,直到整体序列有序。
代码示例:
/**
* 归并排序
*
* @param a
*/
public void mergeSort(int[] a) {
mergeSort(a, 0, a.length - 1);
}
/**
* 将数组划分,然后合并。按照合并规则,进行排序。
*
* @param a 排序的数组
* @param left 左边开始的数组下标
* @param right 右边结束的数组下标
*/
public void mergeSort(int[] a, int left, int right) {
if (left < right) {
int middle = (left + right) / 2;
// 左边的数组
mergeSort(a, left, middle);
// 右边的数组
mergeSort(a, middle + 1, right);
// 合并
merge(a, left, middle, right);
}
}
/**
* 进行合并数组
*
* @param a 需要合并的数组
* @param left 左边的起始下标
* @param middle 中间的下标
* @param right 右边的下标
*/
private void merge(int[] a, int left, int middle, int right) {
int[] tempArray = new int[a.length];
int rightStart = middle + 1;//
int tmp = left;
int third = left;
while (left <= middle && rightStart <= right) {
if (a[left] <= a[rightStart]) {
// tempArray[third++] = a[left++];
tempArray[third] = a[left];
third++;
left++;
} else {
tempArray[third++] = a[rightStart++];
}
}
// 如果左边还有数据需要拷贝,把左边数组剩下的,拷贝到新数组
while (left <= middle) {
tempArray[third++] = a[left++];
}
// 如果右边还有数据需要拷贝,把右边数组剩下的,拷贝到新数组
while (rightStart <= right) {
tempArray[third++] = a[rightStart++];
}
// 将元素拷贝回a数组
while (tmp <= right) {
a[tmp] = tempArray[tmp++];
}
}
8. 基数排序(Radix Sort)
基本思想:将数组按照0-9进行划分,分别进行个位,十位,百位…数的排序。依次按照0-9下标进行排序,总共进行最大数的位数次(例如,百位进行三次),然后汇总成一个整体有序的数列。
实现步骤:
- 找出最大数。计算需要进行排序的次数。=>最大数的位数。
- 创建一个二维数组,里面创建十个一维数组。用于表示0-9的数。
- 遍历数组,通过0-9存储数据。第一次通过个位数0-9依次排序,第二次通过十位数0-9排序…
- 第一次找出各个数个位数的值,将个位为0的数,放在下标为0小数组中,将个位为1的数,放在下标为1小数组中,将个位为2的数,放在下标为2小数组中…依次类推,直到所有数都放到自己的位置。(下标为n的小数组=>即为二维数组中各个小数组对应的下标)
- 第一次找出各个数十位数的值,将十位数为0的数,放在下标为0小数组中,将十位数为1的数,放在下标为1小数组中,将十位数为2的数,放在下标为2小数组中…依次类推,直到所有数都放到自己的位置。
- 继续执行上面的操作,直到执行到最大数的位数。
- 将得到的二维数组中的数依次取出来,序列即为有序数列。
时间复杂度:O(d(r+n))
空间复杂度:O(rd+n)
注:r代表关键字的基数,d代表长度,n代表关键字的个数。
代码示例:
/**
* 基数排序
*
* @param a
*/
public void radixSort(int[] a) {
// 获取最大值,决定后面要比较几趟 有几位数就排几次
int max = a[0];
for (int i = 0; i < a.length; i++) {
if (max < a[i]) {
max = a[i];
}
}
// 次数,记录最大数的位数
int times = 0;
while (max > 0) {
max = max / 10;
times++;
}
// 多维数组
List<ArrayList<Integer>> queue = new ArrayList<ArrayList<Integer>>();
// 二维数组中放十个一维小数组
for (int i = 0; i < 10; i++) {
// 小数组为0-9,用来分0-9模块存储数据
ArrayList<Integer> q = new ArrayList<Integer>();
queue.add(q);
}
// 遍历存储,通过0-9存储数据。第一次通过个位数0-9依次排序,第二次通过十位数0-9排序....
for (int i = 0; i < times; i++) {
for (int j = 0; j < a.length; j++) {
// 获取对应位的值==> i=0:获取个位数,i=1:获取十位数,i=2:获取百位数
int x = a[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
// 获取到所属的小数组下标位置
ArrayList<Integer> q = queue.get(x);
// 把元素添加进对应的下标数组
q.add(a[j]);
queue.set(x, q);
}
// 开始收集
int count = 0;
for (int j = 0; j < 10; j++) {
while (queue.get(j).size() > 0) {
// 拿到每一个数组
ArrayList<Integer> q = queue.get(j);
a[count] = q.get(0);
q.remove(0);
count++;
}
}
}
}
总结
八大排序算法比较: