1、冒泡排序
2、插入排序
3、选择排序
4、希尔排序
5、快速排序
6、归并排序
7、堆排序
题外话
1)以下算法皆升序实现
2)以下算法的思想皆是由我自己的语言描述,描述不当请见谅
3)肯定有错误的地方,我只是一介小白 -.-
1、冒泡排序
冒泡排序对于程序员来说,是一种比较简单也比较常见的排序算法。
冒泡排序思想:比如说有8,3,2,5,1,7,6这7个数,那么冒泡法就是对待排列的一组数之间两两比较。对与每两个数来说,如果前者大于后者,就交换它们的值,将小的放在前面,大的数放在后面,反之不做任何改变,这样,每一次循环结束后,就会将最大的值放到后面,就像吐泡泡一样,将大的值冒上去。
简略图:
第一次循环,对这7个数进行6次交换,交换结果为
第二次
在第一次排完序后,待排数据中最大的数字 8 已经到了最后,不需要再比较。
第二次又会将数组第二大的元素排到第二后。
以此…
由此…
当循环结束后,就可以使数组有序。
核心代码思想:
第一遍,比较六次,将最大的数放到末尾
第二遍,只需要比较五次,就可以将第二大的数放到次末尾
…
对于七个数来说,那我们需要进行六遍比较,也就是arr.length - 1 次(外循环);对于每一遍,进行arr.length - 1 - i 次比较(内循环)。
代码如下(优化版):
public void bubbleSort(int[] arr) {
//判断数组元素个数,少于1个还排什么
if (arr.length == 0 || arr.length == 1) return;
//设置一个boolean变量判断交换是否进行,如果进行交换,下次就不用在进行交换
boolean flag = false;
int temp;
//外循环,7个数最多比较 7-1 次
//每次比较把最大的数放在最后一个,下一次比较的个数减 1
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j + 1] < arr[j]) {
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
//进行了交换
flag = true;
}
//进行了交换
if (flag == true) {//flag==true 等价于 !flag
break;
} else {
flag = false;
}
}
}
}
注:交换两个数还可以如下,不用借助临时变量temp
arr[j + 1] ^= arr[j];
arr[j] ^= arr[j + 1];
arr[j + 1] ^= arr[j];
算法特性:
时间复杂度:O(N^2),两次遍历
空间复杂度:O(1),未借助其它数组
稳定性:稳定,比较两个相等的数时,比较前比较后的前后位置不发生改变。
2、插入排序
思想:将待排序的数组,在你的脑海中,意识中,假装分为两个部分,哪两个部分捏?分为一个有序数组和一个无序数组,且看下面➡⬇
第一遍时,将数组的第一个元素a1看成那个有序表,剩余元素为无序表。第一次遍历时,拿出无序表的第一个元素,也就是数组中第二个元素a2,加入到先前的有序表中。如果a2 > a1,将a2插到a1之后,如果a2 < a1,a1往后挪,将a2放在a1的位置。
第二次,a1、a2组成了有序表,从后面无序表中拿出第一个元素,从a2开始依次比较到a1,找到第一个比a3小大的数,将a3插到这个数后面。
…
以此类推,
直至完成排序。
简略图:
代码如下:
public void insertSort(int[] arr){
//寻找在有序表中的插入位置
int insertIndex;
//无序表里要插入的值
int insertValue;
for (int i = 1; i < arr.length; i++) {
insertValue = arr[i];
insertIndex = i - 1;
//向前查找无序表
//如果要插入的值小于有序表最后一个的值,挪元素,有序表向前遍历
while (insertIndex >= 0 && insertValue < arr[insertIndex]){
//挪元素
arr[insertIndex + 1] = arr[insertIndex];
//有序表向前遍历
insertIndex = insertIndex - 1;
}
//直至找到第一个比 insertValue 小的值
arr[insertIndex + 1] = insertValue;
}
}
算法特性:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
3、选择排序
思想:在本排序中,大多数的做法是假设数组第一个数最小,然后在数组中找到当前最小的值,交换之,这样最小的值就放到了数组第一个。之后,再做第二次选择,第二小放在数组第二个位置上。由此,直至数组遍历完。
优化:双向选择,小的放在左边,大的放右边,同时进行。
简略图:
代码如下:
public void selectSort(int[] arr) {
int min;
int minIndex;
for (int i = 0; i < arr.length - 1; i++) {
//假设当前第一个值最小
min = arr[i];
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
//找到最小值和最小值的下标并更新
if (arr[j] < min) {
min = arr[j];
minIndex = j;
}
}
//最小的值不是假设的当前的下标就交换
if (minIndex != i) {
//交换
//第一个值arr[i] 放到 arr[minIndex]下
arr[minIndex] = arr[i];
//最小的值赋值给arr[i]
arr[i] = min;
}
}
}
优化:
简略图略 -.-
public void selectSort(int[] arr) {
for (int i = 0; i < arr.length / 2; i++) {
//拿每次数组开始的第一个数与之后的数做比较
//保存最大最小值
int min = arr[i];
int max = arr[i];
//保存最大最小值的下标
//思路是,遍历数组,将最小的放在数组前面,大的数放在数组后面
int minIndex = i;
int maxIndex = i;
for (int j = i + 1; j < arr.length - i; j++) {
//找到最小数,和它的下标
//这个最小值下标主要是为了方便将此时数组第一个数与之交换(因为数组要向后遍历的嘛)
if (min > arr[j]) {
min = arr[j];
minIndex = j;
}
//找到最大数,和它的下标
//最大下标与数组最后一个交换
if (max < arr[j]) {
max = arr[j];
maxIndex = j;
}
}
//交♂换
//此时数组第一个数放到 minIndex 下标的位置
arr[minIndex] = arr[i];
//最小的放到此时数组的第一个
arr[i] = min;
//此时数组最后一个数放到 maxIndex 下标的位置
arr[maxIndex] = arr[arr.length - 1 - i];
//保存的 max 放到 此时数组最后一个数的位置,也就是 arr.length - 1 - i 的位置
arr[arr.length - 1 - i] = max;
}
}
算法特性:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定,两个相等的值,会交换它们的前后位置。
4、希尔排序
希尔排序一般用来处理海量数据。
思想:将待排数组按照一个确定的步长分成好几个组,每次对分成的组里面的元素用插入排序法进行排序。逐渐减少组的步长,直至数组有序。
简略图:
代码如下:
public void shellSort(int[] arr) {
//要插入的数
int insertValue;
//要插入数的位置
int insertIndex;
//步长 gap
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
//插排法
insertIndex = i - gap;
insertValue = arr[i];
if (insertValue < arr[insertIndex]) {
while (insertIndex >= 0 && insertValue <= arr[insertIndex]) {
arr[insertIndex + gap] = arr[insertIndex];
insertIndex = insertIndex - gap;
}
arr[insertIndex + gap] = insertValue;
}
}
}
}
算法特性:处理海量数据的排序非常之快。
时间复杂度:O(N^1.3~1.5)
空间复杂度:O(1)
稳定性:不稳定,跳跃式的交换元素,相同元素的前后位置根本不会保证
5、快速排序
(1)简单的最基础的快速排序
顾名思义,慢慢吞吞排序法
思想:
第一次
第一步,找一个基准,先保存这个基准;
第二步,从数组的后面往前找,找到第一个比这个基准小的,放在基准的位置;
第三步,从基准后面一个元素开始找起,找到第一个比基准大的值,放在第二步找到的比基准小的元素的位置上;
注意:在这个过程中,始终 i < j,如果 i = j了,那就循环查找个寂寞~
第四步:将基准放在此时这个 i 或 j 的位置;
这个时候,while 循环结束,在基准前面的元素都小于基准,基准后面的都大于基准;
第五步:递归。
此次排序中,基准为arr[0]。
简略图:
代码如下:
public void quickSort(int[] arr, int low, int high){
//比较懒,就不使用求基准位置的函数
//直接一次就调用递归
//是否需要比较
if (low < high){
int i = low;
int j = high;
//保存基准
int criterion = arr[i];
//左、右指针位置控制
while (i < j){
//从右边找到第一个比基准小的值
//如果基准小,就继续循环
while (i < j && criterion < arr[j]){
j = j - 1;
}
//放在左边
if (i < j){
arr[i++] = arr[j];
}
//从左边找到第一个比基准大的值
while (i < j && criterion > arr[i]){
i = i + 1;
}
//右边咯
if (i < j){
arr[j--] = arr[i];
}
}
arr[i] = criterion;
//对左边进行递归
quickSort(arr, low, i - 1);
//对右边递归
quickSort(arr, i + 1, high);
}
}
算法特性:也是对海量数据的处理非常有感觉。
时间复杂度:O(n * log2 n)
空间复杂度:O(log2 n)
稳定性:不稳定,跳跃式的交换元素。
(2)非递归实现快速排序
划分函数
private int partition(int[] arr, int left, int right) {
int index = -1;
if (left < right) {
int low = left;
int high = right;
int x = arr[low];
while (low < high) {
while (low < high && arr[high] > x) {
high--;
}
if (low < high) {
arr[low] = arr[high];
}
while (low < high && arr[low] <= x) {
low++;
}
if (low < high) {
arr[high] = arr[low];
}
}
arr[low] = x;
index = low;
}
return index;
}
非递归排序
借助栈
public void quickSort(int[] arr, int left, int right) {
Stack<Integer> stack = new Stack<>();
if (left < right) {
stack.push(left);
stack.push(right);
while (!stack.isEmpty()) {
int high = stack.pop();
int low = stack.pop();
int index = partition(arr, low, high);
if (index - 1 > low) {
stack.push(low);
stack.push(index - 1);
}
if (index + 1 < high) {
stack.push(index + 1);
stack.push(high);
}
}
}
}
(3)快速排序的优化及其实现
1)优化:
①对于基准的选取,可以取数组头、数组尾和数组中间三个元素之间的中间值;
或者随机选取基准。
②当序列少于一定长度时,可以采用插入排序进行排序
实现:未完待续
6、归并排序
思想:
说实话,快速排序、归并排序和堆排序算是排序算法里面比较难理解的算法,对于它们的思想无论是课本上还是网络上有比这篇博客更为精准的陈述或者介绍或者说是概括,这篇博客的思想都是通过我自己的语言,力图描述的简单点。
归并排序,先分、再边排边合。
对于待排数据,先将它一分为二,然后,对左边那一半一分为二,对右边那一半也一分为二;继续分割化为两半的子序列,直至不可再分也就是每个序列只有一个元素;
合并,想了想,自己的语言还是描述不清楚,先这样说,将分割到底的待排数据按照规则再合并起来,当最后一次合并过程完成,所得到的数据就是有序数组。
在被学习归并排序的时候,我曾听到这样一句话“按照代码去理解算法”。
也就是先有代码,然后自己再慢慢理解。
所以这里先贴出代码,而后分析逻辑:
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
//左边走
mergeSort(arr, left, mid, temp);
//右走
mergeSort(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;//left 0,也就是第一个子序列开始的下标,走边那一半
int j = mid + 1;//指向第二个子序列开始的下标,右边那一半
int t = 0;//临时数组指针
//将分成的两个序列中的待排元素,比较,升序的填充到 temp 中
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
//下面的两个while循环将两个序列中剩余的元素拷到 temp 的空余位置
//如果左半序列有元素
while (i <= mid) {
temp[t++] = arr[i++];
}
//如果右半部分有元素
while (j <= right) {
temp[t++] = arr[j++];
}
//此时 temp 中是在当前分割、排序、合并后的有序元素,
//并不是整个 temp 中的元素,而是将每次分割的序列拷回 arr
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft++] = temp[t++];
}
//如果是第一次,这是处理递归到最左边的两个数据
}
简略图:
我的理解也很浅薄,我觉得欲说归并排序个清楚,那就得图结合着代码叙述。
mergeSort不用多说,左递归,递归到底右递归,然后依次合并子序列。归并排序的难懂跟它的递归结构也有关系,太难 debug 了,一个盯不准,就不知道递归到哪里了…这里不打算详谈(雾),想要学好排序,可以多多 debug 下~
详谈一下 mergeSort 的马甲函数 merge ,这也是用来做主要工作的,就是对递归到底元素的合并。
说下它的参数里面的 temp 数组,将待排数据分割到底后,合并的时候并不宜将这些元素拷贝在 arr 数组中,这时用一个temp 数组处理,每次合并结束前将 temp 中的元素拷回 arr 里。
代码从上到下依次的作用:
算法特性:同样适合大量数据
时间复杂度:O(n log n)
空间复杂度:O(n) 借助了一个临时数组 temp ,典型的空间换时间系列
稳定性:稳定
7、堆排序
不会