八大排序方法
分类:
- 内部排序:
指将需要处理的所有数据都加载到内部存储器中进行排序。
- 外部排序法:
数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
性能比较
交换排序
冒泡排序
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
俩俩比较,小的换在前,大的换在后面,依次向后循环这个过程
优化
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)
// // 将前面额冒泡排序算法,封装成一个方法
public static void bubbleSort(int[] arr) {
// 冒泡排序 的时间复杂度 O(n^2), 自己写出
int temp = 0; // 临时变量
boolean flag = false; // 标识变量,表示是否进行过交换
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]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
//System.out.println("第" + (i + 1) + "趟排序后的数组");
//System.out.println(Arrays.toString(arr));
if (!flag) { // 在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false; // 重置flag!!!, 进行下次判断
}
}
}
快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据
都比另外一部分的所有数据
都要小
,然后再按此方法对这两部分数据分
别进行快速排序
,整个排序过程可以递归进行
,以此达到
整个数据变成有序序列
。
过程
对6 1 2 7 9 3 4 5 10 8 进行排序
-
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵
j先出动
,这一点非常重要。哨兵j
一步一步地向左挪动
(即j–-
),直到找到一个小于6的数停
下来。接下来
哨兵i
再一步一步向右挪动
(即i++
),直到找到一个数大于6的数停
下来。最后哨兵j停在了数字5
面前,哨兵i停在了数字7
面前。
-
交换
哨兵i和哨兵j所指向的元素的值
。交换之后的序列如下。 6 1 25
9 3 47
10 8
-
到此,第一次交换结束。接下来开始哨兵
j继续向左挪动
(再友情提醒,每次必须是哨兵j先出发
)。他发现了4
(比基准数6要小,满足要求)之后停
了下来。哨兵i
也继续向右挪动
的,他发现了9
(比基准数6要大,满足要求)之后停
了下来。此时再次进行交换
,交换之后的序列如下: 6 1 2 5 4 3 9 7 10 8
- 当i和j指向的数相等时,交换基准数与该该数,到此第一轮“探测”结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。
- 回顾刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
- 通过上面的步骤,使基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。
- 接下来还需要分别处理这两个序列。使6左边和右边的序列有序,只要模拟刚才的方法分别处理6左边和右边的序列即可。
- 首先处理6左边的序列现,左边的序列是“3 1 2 5 4”。将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。
- 因为j往又右挪到动,i往左挪到,会在2的时候碰面,所以2会与3进行交互。调整完毕之后的序列的顺序应该是:“ 2 1 3 5 4”。现在3已经归位。
- 接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下:1 2 3 4 5 6 9 7 10 8
- 对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下:1 2 3 4 5 6 7 8 9 10
- 到此,排序完全结束。我们可以发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面是整个算法的处理过程。
快速排序
之所比较快,因为相比冒泡排序,每次交换是跳跃式的
。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序
一样每次只能在相邻的数之间进行交换
,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了
。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。
代码
//方法1:便于理解
public static void quickSort2(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;//起始位
j=high;
//temp就是基准位
temp = arr[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void quickSort(int[] arr,int left, int right) {
int l = left; //左下标
int r = right; //右下标
//pivot 中轴值
int pivot = arr[(left + right) / 2];
int temp = 0; //临时变量,作为交换时使用
//while循环的目的是让比pivot 值小放到左边
//比pivot 值大放到右边
while( l < r) {
//在pivot的左边一直找,找到大于等于pivot值,才退出
while( arr[l] < pivot) {
l += 1;
}
//在pivot的右边一直找,找到小于等于pivot值,才退出
while(arr[r] > pivot) {
r -= 1;
}
//如果l >= r说明pivot 的左右两的值,已经按照左边全部是
//小于等于pivot值,右边全部是大于等于pivot值
if( l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完后,发现这个arr[l] == pivot值 相等 r--, 前移
if(arr[l] == pivot) {
r -= 1;
}
//如果交换完后,发现这个arr[r] == pivot值 相等 l++, 后移
if(arr[r] == pivot) {
l += 1;
}
}
// 如果 l == r, 必须l++, r--, 否则为出现栈溢出
if (l == r) {
l += 1;
r -= 1;
}
//向左递归
if(left < r) {
quickSort(arr, left, r);
}
//向右递归
if(right > l) {
quickSort(arr, l, right);
}
}
选择排序
选择排序:每趟从待排序的记录中选出关键字最小的记录,顺序放在已排序的记录序列末尾,直到全部排序结束为止。
简单选择排序
- 从第一个元素i开始,默认该元素已经被排序;
- 取出下一个元素j,在已经排序的元素序列中从后往前扫描
- 如果下一个元素j,小于之前已经被排序完的序列的最后一个元素i,则将元素j移动到i的前面
- 重复步骤3,直到之前已经排序完的元素都比小于或者等于下一个元素j
- 将元素j插入到步骤4所找到的那个元素之后
- 重复2到5步骤
举例:对 9 1 2 5 7 4 8 6 3 5进行简单选择排序
通过下面的排序可以发现,是将未排序的部分的最小值取出来,与未排序好的第一个数进行交换
所以排序后为:1 2 3 4 5 5 6 7 8 9
堆排序
堆是具有以下性质的完全二叉树
- 每个结点的值都大于或等于其左右孩子结点的值,称为
大顶堆
; - 每个结点的值都小于或等于其左右孩子结点的值,称为
小顶堆
。
堆排序是将堆中的结点按照宽度优先将其进行编号
,将这种逻辑结构映射到数组中
- 下图为大顶堆的映射
堆排序基本思想:
- 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
- 反复执行123,便能得到一个有序序列了
将4 6 8 5 9进行堆排序
-
第一步:将给出的无序序列
构成一个大顶堆
(一般升序采用大顶堆,降序采用小顶堆
)
1.将无序序列构建为二叉树,按照宽度优先进行构建
2. 从最后一个非叶子结点6开始,从左至右,从下至上进行调整。
3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
4.交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。无需序列构已经造成了一个大顶堆。
-
第二步:将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
1.将堆顶元素9和末尾元素4进行交换
2.重新调整结构,使其继续满足堆定义
3.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
4.按照123步骤,继续进行调整,交换,如此反复进行,最终使得整个序列有序
插入排序
插入排序:每一趟将一个待排序的记录,按照其关键字的大小插入到有序队列的合适位置里,知道全部插入完成
直接插入排序
直接插入排序:是一种最简单的插入排序
其基本操作是:将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表
。
步骤:
- 设数组为a[0…n-1]
- 初始时,
a[0]自动生成一个有序区
,无序区为:a[1...n-1]
。此时i=1,令i为有序区和无序的标记,即i是无序区的第一个数字
- 有序区的最后一个(a[i-1])与无序区的第一个(a[i]),进行比较,
- 如果a[i-1]>a[i],则将a[i]插入到有序区的最后一个,
- 如果a[i-1]<a[i],就a[i]插入到有序区最后一个数字的前面,然后a[i]这个数再与其前一位的数组进行比较,再执行4和5这两个步骤,直到i==n-1;
举例说明:对 9 1 2 5 7 4 8 6 3 5进行直接插入排序
通过比较这些步骤可以发现,插入排序是将未排序的部分的第一个数字,与已排序部分的最后一个数字进行比较,如果小于就在已排序部分进行比较,如果大于就直接放在已排序部分的最后一个
希尔排序
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序
基本思想:该方法实质上是一种分组插入方法
把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
基本步骤:
- 将无序数组划分为group = length/2个组,对这些组进行直接插入排序
- 将组划分为group1 = group/2,再对其进行直接插入排序,依次循环,到groupn
- 直到groupn/2 == 1时,这是最后的一次直接插入排序,排序过后的结果便为排序后的数组
注:为了方便理解,所以才用组来叙述,其实应该是叫做增量,也被称为希尔增量,那些个数组组其实就是增量序列
例题演示:
参考链接
归并排序
基数排序
三大查找方法
顺序查找算法
顺序查找的基本思想:
- 从表的一端开始,顺序扫描表,依次将扫描到的结点关键字和给定值(假定为a)相比较,
- 若当前结点关键字与a相等,则查找成功;
- 若扫描结束后,仍未找到关键字等于a的结点,则查找失败。
- 说白了就是,从头到尾,一个一个地比,找着相同的就成功,找不到就失败。
- 很明显的缺点就是
查找效率低
。适用
于线性表的顺序存储结构
和链式存储结构
。
public static int sequentialSearch(int[] a, int key) {
for ( int i = 0; i < a.length; i++) {
if (a[i] == key)
return i;
}
return - 1;
}
顺序表查找优化
因为每次循环时都需要对i是否越界,即是否小于等于n作判断。事实上,还可以有更好一点的办法,设置一个哨兵,可以解决不需要每次让i与n作比较。看下面的改进后的顺序查找算法代码。
public static int sequentialSearch2(int[] a, int key) {
int index = a.length - 1;
a[ 0] = key; // 将下标为0的数组元素设置为哨兵
while (a[index] != key) {
index--;
}
return index;
}
折半查找
折半查找(Binary Search)技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序)
,线性表必须采用顺序存储
。
折半查找的基本思想是:
- 在有序表中,
取中间记录
作为比较对象
, - 若给定值与中间记录的关键字相等,则查找成功;
- 若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;
- 若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。
- 不断重复上 述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
public static int binarySearch(int[] a, int key) {
int low, mid, high;
low = 0; // 最小下标
high = a.length - 1; // 最大小标
while (low < high) {
mid = (high + low) / 2; // 折半下标
if (key > a[mid]) {
low = mid + 1; // 关键字比 折半值 大,则最小下标 调成 折半下标的下一位
} else if (key < a[mid]) {
high = mid - 1; // 关键字比 折半值 小,则最大下标 调成 折半下标的前一位
} else {
return mid; // 当 key == a[mid] 返回 折半下标
}
}
return -1;
}
最终我们折半算法的时间复杂度为O(logn),它显然远远好于顺序查找的O(n)时间复杂度了。
二分查找特别适用于那种一经建立就很少改动而又经常需要查找的线性表。
分块查找
又称索引顺序查找,这是顺序查找的一种改进方法,用于在分块有序表中进行查找 。
主表:存储数据的表,长度n;
索引表:将主表分块,每块长s,找出每块中的关键字最大值,并且保存该块中所有数据在主表中的索引
(1)分块:将n个数据元素“按块有序”划分为m块。
每一块中的结点不必有序,但块与块之间必须“按块有序”;即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素。每个块中元素不一定是有序的。
(2)根据查找值,和索引表中关键字(每块中的最大关键字)比较,通过对分查找/顺序查找,找到该值所在的块范围;
(3)在相应块中,找到该值在主表中的位置。
平均查找长度ASL<=O(log2(n/s))+s/2 (先对分查找,或顺序查找)