@[TOC] 排序算法
分类
- 线性时间比较类排序:通过比较来决定元素的相对次序,由于时间复杂度不能突破 0 ( n l o g n ) 0(nlogn) 0(nlogn) ,因此称为线性时间比较类排序。其类别:
- 线性时间费比较类排序:不通过比较决定来决定元素的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
具体比较为:
非线性时间比较类排序:
交换排序:冒泡排序、快速排序
插入排序:简单插入排序、希尔排序
选择排序:简单选择排序、堆排序
归并排序:二路归并排序、多路归并排序
线性时间比较类排序:
基数排序
桶排序
术语说明
- 稳定:若 a a a原本在 b b b前面,而 a = = b a==b a==b的情况下进行排序,排序之后 a a a仍然在 b b b前面。
- 不稳定:若 a a a原本在 b b b前面,而 a = = b a==b a==b的情况下进行排序,排序之后 a a a在 b b b后面。
- 内排序:所有排序操作数都在内存中完成
- 外排序:数据量太大,因此把数据放在磁盘中,而排序通过磁盘和内存的传输才能进行
- 算法衡量标准:时间复杂度(一个算法执行所消耗的时间)、空间复杂度(运行完一个程序所需要的内存大小)
算法的详解
1、交换排序
1.1 冒泡排序(稳定排序)
思想: 通过相邻记录的两两比较和交换,使关键字较小的记录像水中的气泡一样逐趟向上漂浮,而关键字较大的记录好比石块一样下下沉,每一趟有一块“最大”的石头沉到水底。
步骤:
1. 将第一条记录的关键字和第二条记录关键字比较,若第一个关键字
≥
\ge
≥第二个关键字,则交换两条记录;然后比较第二条和第三条记录,直至最后一个关键字;
2. 按照步骤1即可完成一趟排序,其结果是最大的关键字排在最后。接着进行第二趟排序,总共需要排序的元素为
n
−
1
n-1
n−1。
3. 按照2即可完成排序,若有
n
n
n个元素,则需要排序
n
−
1
n-1
n−1趟。
代码:
// 1) 冒泡排序: 稳定排序
// -- 时间复杂度:最好0(N),最差:0(N^2)---->平均:O(N^2)
// -- 空间复杂度:O(1)
// -- 稳定排序
int [] bubleSort(int primitives[]) {
// 数组长度小于或者等于1
if (primitives.length <= 1)
return primitives;
// 总的循环趟数,假设数组元素为n,则loop = n-1
for (int loop = 0; loop < primitives.length - 1; loop++) {
boolean isSequence = true; // 增加标记位,若某一趟发现待排序的数组已经完全有序了,则无需排序直接退出即可。
// 按照冒泡思想,每一趟最大值排在最后面,因此在第i趟,最后的n-loop后面元素无需比较,已经有序.因此比较的次数为:n-loop-1
for (int i = 0; i < primitives.length - loop - 1; i++) {
if (primitives[i] > primitives[i + 1]) { // 稳定排序,相等不改变与原顺序
// 使用变量进行两个元素的交换
// int temp = primitives[i];
// primitives[i] = primitives[i+1];
// primitives[i+1] = temp;
// 位运算交换数据
primitives[i] = primitives[i] ^ primitives[i + 1];
primitives[i + 1] = primitives[i] ^ primitives[i + 1];
primitives[i] = primitives[i] ^ primitives[i + 1];
isSequence = false;
}
}
if(isSequence)
break;
}
return primitives;
}
1.2 快速排序(import)
思想: 是对冒泡排序算法的一种改进。我们从数组中选择一个元素,把这个元素称之为 中轴元素 吧,然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,我们无需再移动中轴元素的位置。在进行完第一轮后,再按照同样的思想对左右两部分的元素进行同样的操作。
步骤:设待排序记录存于
r
[
t
]
r[t]
r[t],
r
[
t
+
1
]
r[t+1]
r[t+1],…,
r
[
w
]
r[w]
r[w]中。
1). 初始化两个变量
i
=
t
i=t
i=t和
j
=
w
j=w
j=w,选择基准值
p
r
i
v
o
t
=
r
[
t
]
privot = r[t]
privot=r[t]
2). 先从最右边
j
=
w
j=w
j=w开始逐个和基准值进行比较,若发现
r
[
j
]
r[j]
r[j]小于
p
r
i
v
o
t
privot
privot则说明需要交换两者位置,此时将
r
[
i
]
=
r
[
j
]
r[i] = r[j]
r[i]=r[j], i++;
3). 继第2)步后,开始从
i
i
i的位置逐个向后扫描,直至r[i] 大于
p
r
i
v
o
t
privot
privot, 则说明需要交换位置,将
r
[
j
]
=
r
[
i
]
r[j] = r[i]
r[j]=r[i]且j++;
4) . 如此重复2)和3)步,最终当
l
o
w
>
=
h
i
g
h
low >= high
low>=high时,完成一趟排列,最终赋值:r[low] = privot。
5) . 经过第4)步即可将一个序列分成左右两部分,按照递归处理左右两部分。
代码:
// 2)快速排序
void quickSort(int prim[], int low, int high){
// 左索引 小于 右索引
if(low < high ) {
int baseIndex = partition(prim,low,high);
quickSort(prim,low,baseIndex-1);
quickSort(prim,baseIndex+1,high);
}
}
/**
* 进行完一轮排序,并将数组分成两部分,将基准索引返回,供下趟排序使用
* @describe:具体每一轮的实现,将low位置对应的值作为基值,最终分成两部分,作部分小于基值右部分大于基值
* @param prim
* @param low
* @param high
* @return
*/
private int partition(int[] prim, int low, int high) {
int privot = prim[low]; // 将数组中第一个元素作为基准值,标记基准值
/**
* 值得注意的是:在进行比较时,不要每一步去交换基准值,因为基准值最终所在的位置一直在调整,没有固定
* 一定的位置,因此避免交换次数过多,不要交换基准值,优化算法如下:
*/
while(low < high) { // 两个索引不一致,需要调整
// 若右部high索引对应的value小于或者等于左部low索引对应的值,则逐步减
while(low < high && prim[high] >= privot) high--;
// 直至 到了 high对应的value值小于low对应的value值,此时将high对应的值赋值给low位置
prim[low] = prim[high];
// 经过上一步,除了low==high,那一定有prim[low]<privator,因此low逐步增加
while(low < high && prim[low] <= privot) low++;
// 直至 带了low对应的值大于high对应的值,则此时将low对应的值赋值给high对应的位置
prim[high] = prim[low];
}
if(low != high)
System.out.println("Error, the value of low is not equal with the hign value");
prim[low] = privot;
return low;
}
分析: 从算法可知,快速排序属于不稳定排序。
-时间复杂度: O(nlogn)
-空间复杂度: O(logn)
2、选择排序
2.1 简单选择排序
**思想:**每次从待排序列中选择最小(最大,看排列方式)关键字,顺序放在已经排好的序列中,直到序列已经全部有序。
**步骤: **
1)查找待排序列中最小的关键字,并将它和该区间的第一个关键字交换位置;(一层循环,找最小关键字)
2)重复
n
−
1
n-1
n−1次1)步骤,直至序列已经有序。(二层循环,需要的循环趟数)
代码:
// 1) 简单选择排序:
// a) n-1趟
// b) 每一趟选择最小的元素并与无序部分第一个元素进行交换
public void simpleSelectSort(int [] prim) {
int len = prim.length;
if(len < 2)
return;
int min_index = 0;
for(int i = 0; i < len -1 ; i++) { // 循环的趟数,每一趟确定i位置的元素
min_index = i; // 默认无序部分第一个元素是最小元素
for(int j = i +1; j<len; j++){
if(prim[j] < prim[min_index])
min_index = j; // 更新最小值索引
}
// 这一趟已找出最小值,因此进行i位置与min_index的值交换
int temp = prim[i];
prim[i] = prim[min_index];
prim[min_index] = temp;
}
}
分析:不稳定
- 时间复杂度:
O
(
N
2
)
O(N_2)
O(N2) 最好最坏都是如此
- 空间复杂度:
O
(
1
)
O(1)
O(1)
2.2 堆排序
**思想:**主要是借助二叉树的结构调整,分为以下两步:构建堆(大根堆、小根堆)、交换首尾两元素继续讲剩余的元素构建堆
步骤:
1)构建堆(大根堆为例):根节点值大于左右节点值。基于层次遍历与数组对应索引关系,当根节点值索引为
i
i
i时,左右节点值
的索引分别为:
2
∗
i
+
1
2*i +1
2∗i+1、
2
∗
i
+
2
2*i+2
2∗i+2。每次挑选最大的值放在根节点,循环
l
e
n
/
2
len/2
len/2次。
2)交换再调整堆:交换根节点值和最后元素的值,再按照
1
)
1)
1)中的步骤调整成大根堆,循环len-1次。
代码:
/**
* 1) 堆排序:
* 第一步:构成堆(大根堆、小根堆)。
* 第二步:将根元素与最后一个元素交换,再将剩余数据调整成堆
* 大根堆:根节点值大于左右节点值(适合升序排序)、小根堆:根节点值小于左右节点值(适合降序排序)
* 因此代码实现主要分为两部分:构堆、交换再构堆
* a) 构堆:由于调整堆是二叉树,因此调整堆得轮数:len/2-1。假设根节点编号为i,则左节点编号2*i+1,右节点2*i+2
* 从根、左右节点选择最大的节点放到根节点上。
*/
public void heapSelectSort(int [] prim){
int len = prim.length;
if(len < 2)
return;
// 通过数组进行构建堆、大根堆(升序的方式)
for(int i = len/2-1; i>=0; i--) {
constructBigHeap(prim, i,len);
}
// 此时prim的数组满足大根堆要求,即可以用它与最后一个元素进行交换
for(int j= len-1; j>0; j--) {
swap(prim, 0, j);
constructBigHeap(prim,0,j);
}
}
/**
* @descri 交换数据
* @param prim
* @param i
* @param j
*/
private void swap(int[] prim, int i, int j) {
int temp = prim[i];
prim[i] = prim[j];
prim[j] = temp;
}
/**
* @descri 构建大根堆
* @param prim: 待构建的数组
* @param i:i表示当前的根节点
* @param len:len表示prim数组待调整的长度
*/
private void constructBigHeap(int[] prim, int i,int len) {
int temp = prim[i]; // 记住当前的根节点值
for(int k = 2*i+1; k<len; k=2*k+1){
if(k+1 < len && prim[k] < prim[k+1]) // 比较左右节点,选组最大值
k++; // k 记住左右节点最大值索引
if(temp < prim[k]) { // 在与根节点进行比较,挑选根、左右节点最大值
prim[i] = prim[k];
i = k; // 更换根节点位置
}
else
break; // 说明该数组已经满足大根堆,无需调整
}
prim[i] = temp;
}
分析:不稳定
- 时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)最好、最坏也是如此.
- 空间复杂度:
O
(
1
)
O(1)
O(1)
3、归并排序
3.1 归并排序
思想: 从上至下递归拆分、从下至上递归返回合并。
步骤:
1)递归拆分:先把待排序数组分为左右两个子序列,再讲子序列左右划分为4个子序列,直至最小序列的个数为1。
2)层级合并:从下到上层级合并,也可以理解递归层级返回。先作部分排序合并,再右部分排序合并,最终左右部分排序合并,直至最顶层。
代码:
/**
* @describe: 归并排序
* @param prim 待拆分数组
* @param left 待拆分数组最小下标
* @param right 待拆分数组最大下标
*/
public void mergeSort(int prim[], int left, int right) {
int mid = (left + right) / 2; // 中间位置索引
if(left < right) { // 递归终止条件 left >= right
mergeSort(prim, left, mid); // 递归拆分左边
mergeSort(prim, mid+1, right); // 递归拆分右边
mergeProcess(prim, left, mid, right); // 合并左右
}
}
/**
* @describe: 对左右两部分进行按照从小到大的顺序排序,即合并左右两部分
* @param prim 待合并的数组
* @param left 待合并数组左部分的最小索引值
* @param mid 待合并数组中间位置的索引
* @param right 待合并数组右部分做大索引值
*/
private void mergeProcess(int[] prim, int left, int mid, int right) {
int [] temp = new int[right - left + 1]; // 临时数组,用于保存每次合并之后的结果
int i = left; // 左部分第一个元素的索引
int j = mid + 1; //右部分第一个元素的索引
int k = 0; // 临时数组的索引
while(i <= mid && j <= right) { // 这个循环先排序出左右部分合并后较小序列,结束时肯定有一部分已经没有待排序的数
if(prim[i] <= prim[j])
temp[k++] = prim[i++];
else
temp[k++] = prim[j++];
}
// 至此,至少有一部分元素已经全部在temp中即:i> mid 或者 j>right
while(i <= mid) { // j > right
temp[k++] = prim[i++];
}
while(j <= right) {
temp[k++] = prim[j++];
}
// 将临时数组重新复制到prim数组中
for(int n =0; n<temp.length; n++)
prim[left + n] = temp[n];
}
分析:稳定
— 时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
— 空间复杂度:
O
(
n
)
O(n)
O(n)