排序
一、冒泡排序
(一)原理
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
(二)代码实现
public class MyTest {
public static void main(String[] args) {
int[] a={24,69,80,57,13};
int t=0;
for (int i = 0; i < a.length-1; i++) { //外层循环用于定义排序次数
for(int j = 0;j < a.length-i-1; j++){ //j < a.length-i-1 定义每次排序比较的次数
if(a[j]>a[j+1]){
t=a[j];
a[j]=a[j+1];
a[j+1]=t;
}
}
}
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
二、选择排序
(一)原理
第一种:从0索引开始,逐个和后面的所有元素进行比较,小的往前放,第一轮排序完后,最小的元素出现在了最小索引处;第二轮排序,从1索引开始…
第二种:第一次设定起始位置的元素为最小(大)元素,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的首部。以此类推,直到全部待排序的数据元素的个数为零。
(二)代码实现
第一种
public class MyTest {
public static void main(String[] args) {
int[] a={24,69,80,57,13};
int t=0;
for (int i = 0; i < a.length-1; i++) { //外层循环定义排序次数 i为每次选择排序选择的元素的索引
for(int j = i+1;j < a.length; j++){ //每次从索引为i的元素后开始比较
if(a[i]>a[j]){
t=a[i];
a[i]=a[j];
a[j]=t;
}
}
}
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
第二种
public class MyTest {
public static void main(String[] args) {
int[] a = {46, 55, 13, 42, 17, 94, 5, 70, 10, 30, 56, 1, 0, 20, 1, 5, -1};
for (int i = 0; i < a.length-1; i++) {//确定每次开始的位置
int min=a[i];//假设每次起始的元素就是最小值
int flag=i;//标记最小值所在的位置
for(int j=i+1;j<a.length;j++){
if(a[j]<min){//从开始索引向后一个个和min比较,再把最小值存放到min,并更新flag标记
min=a[j];
flag=j;
}
}
if(min!=a[i]){//如果min和起始的索引的元素值不一致了,则交换min所在的索引元素和起始索引的元素
a[flag]=a[i];
a[i]=min;
}
}
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}
三、直接插入排序
(一)原理
直接插入排序,是一种最简单的排序方法。他的基本操作是将一个记录插入到一个长度为m 的有序表中,使之仍保持有序,从而得到一个新的长度为m+1的有序列表。假设有一组无序元素{k1,k2…,kn},排序开始就认为k1是一个长度为1的有序序列,让k2按照大小顺序插入上述k1这个有序序列,使之成为一个表长为2的有序序列,然后让k3按照大小顺序插入上述表长为2的有序序列,使之成为一个表长为3的有序序列,以此类推,最后让kn插入表长为n-1的有序序列,得到一个表长为n的有序序列。
例如:
49,38,65,97,76,13,27 原始数据
[49],38,65,97,76,13,27 从1索引开始
[38,49], ,65,97,76,13,27
[38,49,65] 97,76,13,27
[38,49,65,97] 76,13,27
[38,49,65,76,97]13,27
[13,27,38,49,65,76,97],27
[13,27,27,38,49,65,76,97]
(二)代码实现
public class MyTest {
public static void main(String[] args) {
int[] a={24,69,80,57,13};
for (int i = 1; i < a.length; i++) {//外层循环定义轮次
for(int j=i;j>0;j--){//里层就是循环让当前元素和上一个有序列表中的每个元素比较,进行位置交换,使之仍保持有序
if(a[j]<a[j-1]){
int t=a[j];
a[j]=a[j-1];
a[j-1]=t;
}
}
}
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
四、希尔排序
(一)原理
其实希尔排序也是一种插入排序。
希尔排序又称缩小增量排序。它先将原数组按增量(间隔)ht分组,每个分组按直接插入法排序。排完后用下一个增量ht/2再进行分组,再直接插入排序。直到ht=1时插入排序完成后,数组有序。
关键:选择合适的增量。
主要特点:
- 每一趟以不同的增量进行插入排序,当增量ht较大时,被移动的记录是跳跃式进行的,到最后一趟排序时(ht=1),大部分记录已经有序,不需要多少移动,所以提高了排序的速度。
- 希尔排序的时间复杂度分析与选择的增量有关系,它是不稳定的排序方法。
(二)代码实现
public class MyTest {
public static void main(String[] args) {
int[] a = {46, 55, 13, 42, 17, 94, 5, 70, 10, 30, 56, 1, 0, 20, 1, 5, -1};
for (int d = a.length / 2; d >= 1; d = d / 2) {//最普通的增量选择就是使用数组长度的一半 比较完一轮后改变增量
for (int i = d; i < a.length; i++) {//其实直接插入排序就是增量为1的希尔排序
for (int j = i; j >= d; j = j - d) {//里层就是循环让当前元素和上一个有序列表中的每个元素比较,进行位置交换,使之仍保持有序
if (a[j] < a[j - d]) {
int t = a[j];
a[j] = a[j - d];
a[j - d] = t;
}
}
}
}
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}
增量的合理选择:
最普通的选择就是使用数组长度的一半
这种增量从效率上来说,不是非常好
对于特别大的数组如果增量(间隔)还是选择数组长度的一半,并不断除2直到增量为1,那么比较的次数也会非常多,效率低。我们可以用Knuth序列来进行增量的选择。
Knuth序列:1,4,13,40,… ,3*d+1(d为上一个增量的大小)
举例来说,有一个包含1000个数据的无序数组,首先在一个循环中使用序列的生成公式来计算出最初的增量:d值最初被赋为1,然后应用公式d=3*d+1生成序列1,4,13,40,121,364,1093…当间隔大于数组大小的时候,这个过程停止。对于一个含有1000个数据的数组,1093就太大了。因此,使用序列的364作为最大的数字来开始这个排序过程。然后,每完成一次排序全程的外部循环,用前面提供的此公式倒推式来减小间隔:d=(d-1)/3。这个倒推的公式生成逆置的序列364,121,40,13,4,1。从364开始,以每一个数字作为增量进行排序。当数组用1增量排序完后,算法结束。
希尔排序比插入排序快很多,它是基于什么原因呢? 当d值大的时候,数据项每一趟排序需要移动元素的个数很少,但数据项移动的距离很长。这是非常有效率的。当d逐渐减小时,此时数据项已经大致有序了,这时要移动的元素也相对少了,这相比于插入排序可以更有效率。
使用Knuth序列的希尔排序
public class MyTest {
public static void main(String[] args) {
int[] a = {46, 55, 13, 42, 17, 94, 5, 70, 10, 30, 56, 1, 0, 20, 1, 5, -1};
int startIncrement = 1;//设置起始增量
while (startIncrement < (a.length - 1)/3) {//找到符合该无序数组长度的合理增量
startIncrement = 3 * startIncrement + 1;
}
System.out.println(startIncrement);
for (int d = startIncrement; d >= 1; d = (d - 1) / 3) {
for (int i = d; i < a.length; i++) {
for (int j = i; j >= d; j = j - d) {
if (a[j] < a[j - d]) {
int t = a[j];
a[j] = a[j - d];
a[j - d] = t;
}
}
}
}
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}
五、归并排序
(一)原理
归并排序(Merge Sort)就是利用归并的思想实现排序的方法。它的原理是假设初始序列有N个记录,则可以看成是N个有序的子序列,每个子序列的长度为1,然后两两归并,得到 N/2 个长度为2或1的有序子序列,再两两归并…
如此重复,直至得到一个长度为N的有序序列为止,这种排序方法称为2路归并排序。
1.分而治之
归并排序,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之
2.合并有序的子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
(二)代码实现
public class MyTest {
public static void main(String[] args) {
int[] a = {46, 55, 13, 42, 17, 94, 5, 70, 10, 30, 56, 1, 0, 20, 1, 5, -1};
chaiFen(a,0,a.length-1);
//打印结果
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static void chaiFen(int[] a,int startIndex,int endIndex) {//递归拆分,拆分好后逐步归并
//计算中间索引
int middleIndex=(startIndex+endIndex)/2;
if(startIndex<endIndex){
//递归拆分左边
chaiFen(a,startIndex,middleIndex);
//递归拆分右边
chaiFen(a,middleIndex+1,endIndex);
//归并
merge(a,startIndex,middleIndex,endIndex);
}
}
private static void merge(int[] a,int startIndex,int middleIndex,int endIndex) {
int[] t=new int[endIndex-startIndex+1];//定义临时数组
int i=startIndex;//定义第一个数组的开始索引
int j=middleIndex+1;//定义第二个数组的开始索引
int index=0;//定义临时数组的开始索引
//遍历数组进行大小比较,使之有序放入临时数组
while(i<=middleIndex&&j<=endIndex){
if(a[i]<=a[j]){
t[index]=a[i];
i++;
index++;
}else {
t[index]=a[j];
j++;
index++;
}
}
/*因为两个数组的的元素个数,不是均等的,通过上面循环比较完后会有
剩余元素,可能第一个数组元素会有剩余,也可能第二个数组元素会有剩余
*/
while(i<=middleIndex){
t[index]=a[i];
i++;
index++;
}
while(j<=endIndex){
t[index]=a[j];
j++;
index++;
}
//然后我们遍历临时数组,将临时数组中的元素,放置到原数组中
for (int k = 0; k < t.length; k++) {
//注意这里k要加上起始索引
a[k + startIndex] = t[k];
}
}
}
六、快速排序
(一)原理
- 先从数列中取出一个数作为基准数。
- 将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
举例:
解析:首先将第一个元素6作为基准数,起初i和j索引分别在数组的首和尾,从j开始从后向前找比基准数6小的值,找到后停下,于是j在值5处停下;接着,i开始从前向后找比基准数大的数,找到后停下,于是i在值为7处停下,然后交换5和7。再接着,j继续向前移,找到比基准数小的4,然后i继续向后移,找到比基准数大的9,将4和9交换。接着,j继续前移,找到比基准数小的3,i再开始后移,也到了3的位置,i和j重合了,于是,将3和基准数6交换(i和j重合,就将基准数和该处元素交换)。至此,基准数6就排到了自己正确的位置,然而6两边的数还是无序的,因此又拆分成两个数组,重复上述的操作,直到所有的数都归位为止。
(二)代码实现
public class MyTest {
public static void main(String[] args) {
int[] a = {46, 55, 13, 42, 17, 94, 5, 70, 10, 30, 56, 1, 0, 20, 1, 5, -1};
quickSort(a, 0, a.length - 1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static void quickSort(int[] a, int startIndex, int endIndex) {
if (startIndex < endIndex) {
//获取归位后的基准数形成的左右分区的索引
int index = getIndex(a, startIndex, endIndex);
//对左右两个分区 再进行同样的步骤 ,即递归调用
quickSort(a, startIndex, index - 1);
quickSort(a, index + 1, endIndex);
}
}
private static int getIndex(int[] a, int startIndex, int endIndex) {
int index = startIndex;//定义基准数的索引
int i = startIndex;
int j = endIndex;
int t = 0;//临时变量
while (i < j) {
while (i < j && a[j] >= a[index]) {//从右往左比较,找到比基准数小的数
j--;
}
while (i < j && a[i] <= a[index]) {//从左往右比较,找到比基准数大的数
i++;
}
//交换两个数
t = a[i];
a[i] = a[j];
a[j] = t;
}
//此时i=j,交换基准数和a[i]
t = a[i];
a[i] = a[index];
a[index] = t;
return i;//返回此时基准数所在的索引
}
}
七、基数排序
(一)原理
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
举例:先将原始数组的元素按个位数以在数组中的顺序分别放进对应的“桶”中,例如750个位数为0,则放进0的“桶”中。所有数放完后再按放入桶中的顺序取出来,再进行十位数的比较,重复以上操作,再进行百位数的比较…
(二)代码实现
public class MyTest {
public static void main(String[] args) {
//基数排序:分配再收集,元素之间不比较大小
int[] a = {1, 2, 45, 90, 43, 65, 87, 99, 234, 987, 154, 92, 12,19000,1768,2909};
//进行基数排序
sortArray(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static void sortArray(int[] a) {
//定义一个二维数组,里面放十个桶
int[][] tempArr = new int[10][a.length];
//定义一个一维数组,用于统计十个桶中各有多少元素
int[] counts = new int[10];
//获取数组中的最大值
int max = getMax(a);
//获取最大数的位数,确定我们要分配收集的轮次
int len = String.valueOf(max).length();
for (int i = 0, n = 1; i < len; i++, n *= 10) {
for (int j = 0; j < a.length; j++) {
int ys = a[j] / n % 10;//获取每个位上的数字
//根据每个位上的数字放入对应的桶中
tempArr[ys][counts[ys]++] = a[j];
}
//每一轮排序完后,按顺序取出桶中的数据
int index = 0;
for (int k = 0; k < counts.length; k++) { //k代表桶0-9
if (counts[k] != 0) {
for (int h = 0; h < counts[k]; h++) {
//取出桶中的数据,放回原数组
a[index] = tempArr[k][h];
index++;
}
//取完之后,清除上次统计的个数,以进行下一轮排序
counts[k]=0;
}
}
}
}
//找出数组中的最大数,判断其位数以确定排序轮次
private static int getMax(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
return max;
}
}
细心的朋友可能会问:如果数组中有负数怎么办?
那么我们可以首先找到数组中的最小负数值,然后让所有数字加上这个负数值的绝对值,保证所有数字都为非负,再进行基数排序,最后再让所有数字减去刚刚加上的绝对值就好了。
public class MyTest11 {
public static void main(String[] args) {
//基数排序:分配再收集,此排序方法元素之间不比较大小
int[] a = {1, 2, 45, -90, 43, 65, -87, 99, -234, 987, -154, 92, 12,19000,1768,2909};
int min = getMin(a);
if(min<0){
for (int i = 0; i < a.length; i++) {
a[i]=a[i]+Math.abs(min);
}
//进行基数排序
sortArray(a);
for (int i = 0; i < a.length; i++) {
a[i]=a[i]-Math.abs(min);
System.out.print(a[i] + " ");
}
}else{
//进行基数排序
sortArray(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}
private static void sortArray(int[] a) {
//定义一个二维数组,里面放十个桶
int[][] tempArr = new int[10][a.length];
//定义一个一维数组,用于统计十个桶中各有多少元素
int[] counts = new int[10];
//获取数组中的最大值
int max = getMax(a);
//获取最大数的位数,确定我们要分配收集的轮次
int len = String.valueOf(max).length();
for (int i = 0, n = 1; i < len; i++, n *= 10) {
for (int j = 0; j < a.length; j++) {
int ys = a[j] / n % 10;//获取每个位上的数字
//根据每个位上的数字放入对应的桶中
tempArr[ys][counts[ys]++] = a[j];
}
//每一轮排序完后,按顺序取出桶中的数据
int index = 0;
for (int k = 0; k < counts.length; k++) { //k代表桶0-9
if (counts[k] != 0) {
for (int h = 0; h < counts[k]; h++) {
//取出桶中的数据,放回原数组
a[index] = tempArr[k][h];
index++;
}
//取完之后,清除上次统计的个数,以进行下一轮排序
counts[k]=0;
}
}
}
}
//找出数组中的最大数,判断其位数以确定排序轮次
private static int getMax(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
return max;
}
//找出数组中的最小数
private static int getMin(int[] a) {
int min = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] < min) {
min = a[i];
}
}
return min;
}
}
八、堆排序
声明:本节转载自Fuzz_的博客https://blog.csdn.net/qq_36186690/article/details/82505569
(一)原理
在讲堆排序之前介绍几个概念:
- 叶子结点:一棵树当中没有子结点(即度为0)的结点称为叶子结点
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是(2^k) -1 ,则它就是满二叉树。(除最后一层无任何子结点外,每一层上的所有结点都有两个子结点的二叉树。)
- 完全二叉树:若一个二叉树有k层,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,且第k 层所有的结点都连续集中在最左边,这就是完全二叉树。满二叉树是完全二叉树的一种。
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆的两种表现形式:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对大顶堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
我们发现,一个索引为i的结点,它的左右结点的索引分别为2*i+1和2*i+2。
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]
小顶堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]
堆排序基本思想:
- 将待排序列构成一个大顶堆(或者小顶堆),升序用大顶堆,降序用小顶堆;
- 将堆顶元素与堆尾元素交换,并断开(从待排序列中移除)堆尾元素,该元素就为最大(小)值。
- 用剩下的元素重新构建大顶堆(小顶堆)。
- 重复2~3,直到待排序列中只剩下一个元素。
步骤演示:
步骤一 将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构如下
2.此时我们从第一个非叶子结点开始(叶子结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。由于[6,9,5]中9最大,因此6和9交换。
4.找到第二个非叶子结点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足大顶堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
(二)代码实现
public class HeapSort {
public static void main(String []args){
int []arr = {1, 2, 45, -90, 43, 65, -87, 99, -234, 987, -154, 92, 12,19000,1768,2909};
sort(arr);
System.out.print("排序后:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){//非叶子结点的查找顺序:从下至上
//从上至下,从左至右调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整 因为其他元素都是有序的,就堆顶被换过 无序,因此从0索引开始重新构建大顶堆
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr 待调整的数组
* @param i 表示当前非叶子结点在数组中的索引
* @param length 表示有多少个元素需要继续调整, length在不断减小
*/
public static void adjustHeap(int []arr,int i,int length){
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
//下面这段代码用于找到i结点的最大的子结点 如果左子结点小于右子结点,k指向右子结点 k+1>=length就溢出 没必要比较了
if(k+1<length && arr[k]<arr[k+1]){
k++;
}
if(arr[k] >arr[i]){//如果子结点大于父结点,交换父子结点
swap(arr,i,k);
// 如果子结点更换了,那么,以子结点为根的子树会受到影响,所以,循环对子结点所在的树继续进行判断
i = k;
}else{
break;
}
}
}
/**
* 交换元素
* @param arr 需要交换元素所在的数组
* @param a 需要交换的元素索引
* @param b 需要交换的元素索引
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}