目录
冒泡排序
冒泡排序算法的基本思想是通过对待排序序列从前向后,依次比较相邻的元素值,若发现逆序
则交换,使值大的元素从前移向后部,因为排序过程中各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个flag判断元素是否交换。从而减少不必要的比较
ps:
一共进行数组的大小-1次的循环
每一趟排序的次数在逐渐的减少
小优化:当在一次排序过程中交换次数为0时,说明已经有序,可提前结束冒泡排序。
我们会发现,每一次排序之后至少有一个元素会到最终位置上,就是每次排序的元素中的最大元素。
冒泡排序是稳定的排序。最好时间复杂度为O(n),最差时间复杂度为O(n^2),平均时间复杂度为O(n^2)。
public class 冒泡排序算法 {
public static void main(String[] args) {
int arr[]={3,9,-1,10,-2};
bubble(arr);
System.out.println("排序后");
System.out.println(Arrays.toString(arr));
}
//封装成一个方法
public static void bubble (int[]arr) {
int temp = 0;
boolean flag = false;
// 时间复杂度为(o^2)
for(int j = 0; j < arr.length-1; j++){
for (int i = 0; i < arr.length-1-j; i++) {
if (arr[i]>arr[i+1]) {
//交换操作
flag=true;
temp = arr[i];
arr[i]=arr[i+1];
arr[i+1]=temp;
}
}
// System.out.println("第"+(j+1)+"趟排序后的数组");
// System.out.println(Arrays.toString(arr));
if (!flag) {//在一趟排序中,一次交换都没有发生过
break;
}else {
flag= false;
//重置,下次进行判断
}
}
}
}
选择排序
属于内部排序,每次都找最小值往前依次放,次数为数组大小-1
每一轮排序又是一个循环
//循环规则
1. 先假定当前这个数是最小数
2. 然后和后面每个数进行比较,如果发现有比当前更小的数字,就重新定最小数字然后得到下标
3. 当遍历数组到最后的时候就得到本轮最小数和下标
4. 交换
选择排序是不稳定的排序方法。最差时间复杂度为O(n^2),平均时间复杂度为O(n^2)。
//要求对一串数据进行排序
public class 选择排序 {
public static void main(String[] args) {
int arr[]={101,1,23,30,51,40};
select(arr);
}
public static void select(int []arr) {
//1
for(int i=0;i<arr.length-1;i++){
int minIndex =i;
int min = arr[i];
for (int j=i+1;j<arr.length;j++){
if (min>arr[j]) {
min=arr[j];
minIndex = j;
}
}
arr[minIndex]=arr[i];
arr[i]=min;
System.out.println("第"+(i+1)+"轮后~");
System.out.println(Arrays.toString(arr));
}
}
}
插入排序
插入排序是稳定的排序。最好时间复杂度为O(n),最差时间复杂度为O(n^2),平均时间复杂度为O(n^2)
public class 插入排序 {
public static void main(String[] args) {
int[] arr={101,34,119,1,-1,89};
insertsort(arr);
}
public static void insertsort(int []arr){
// 定义待插入的数
for(int i = 1 ; i<arr.length;i++){
int insertVal = arr[i];
int insertIndex = i-1;//即arr【1】前面数的下标
// 寻找插入位置
// 1.insertVal>=0,防止越界
// 2.insertIndex<arr[insertIndex] 待插入的数,还没有找到插入位置
// 3.就需要将arr[insertIndex]后移
//
while (insertIndex>=0&&insertVal<arr[insertIndex]) {
arr[insertIndex+1]= arr[insertIndex];
insertIndex--;
}
// 当退出while循环时意味着找到位置
arr[insertIndex+1]=insertVal;
System.out.println("第"+i+"轮插入后");
System.out.println(Arrays.toString(arr));
}
}
}
希尔排序
希尔排序是插入排序的一种,又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版。
希尔排序是把整个数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时}},整个分组恰被分成一组,算法便终止。希尔排序又分为位移法和交换法,下代码为交换法
public class 希尔排序交换法慢 {
public static void main(String[] args) {
int arr[]={8,9,1,7,2,3,5,4,6,0};
shellsort(arr);
}
//拥有两种方式 交换法 位移法 下面是交换法
public static void shellsort(int arr[]) {
int temp = 0;
int count=0;
for(int gap = arr.length/2 ; gap > 0 ; gap /= 2){
for(int i = gap;i<arr.length;i++){
// 遍历各组中所有的元素
for(int j = i-gap;j>=0;j-=gap){
// 如果当前元素大于加上步长后的那个元素,说明需要交换
if(arr[j]>arr[j+gap]){
temp = arr[j];
arr[j]=arr[j+gap];
arr[j+gap]=temp;
}
}
}
System.out.println("希尔排序第"+(++count)+"轮后的情况="+Arrays.toString(arr));
}
}
}
希尔排序是非稳定的排序。对希尔排序的时间复杂度分析很困难,在特定情况下可以准确的估算排序码的比较次数和元素移动的次数,但要想弄清楚排序码比较次数和元素移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。
快速排序
快速排序的基本思想:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序分为数组划分和递归排序两个步骤。
1.数组划分
选取一个基值,将数组分为大于基值以及小于基值两部分,并返回基值所在位置以利用于递归划分。 对数组a,设需要划分的一段为a[p]~a[r],我们期望得到一个q,其中p<=q<=r,使得a[p]~a[q-1] <=a[q]<= a[q+1]~a[r],这个时候原先的一段数组被分成了三部分。我们可以设基值x为这段数组的第一个元素a[p]。然后令i=p+1,j=r。当a[j]>=x时,j--;当a[j]<x时,我们需要将这个元素放到小于基值的一边,于是开始比较a[i] : 当a[i]<=x时,i++;当a[i]>x时,交换此时a[i]与a[j]的元素。判断直到i==j时结束,交换基准值与a[i]。这一部分算法复杂度为o(n) 。
2.递归排序
在对整个数组进行了划分后,我们将数组分成了两部分,一部分比基值小,一部分比基值大,并且我们知道了基值所在的位置,因此只需对划分出来的两部分进行递归排序即可。
对于分治算法,当每次划分时,若都能分成两个等长的子序列,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。除了取序列的第一个或最后一个元素作为基准,还可以随机选取基准和三数取中选择基准。
快速排序是不稳定的排序方法。最差时间复杂度为O(n^2),平均时间复杂度为O(nlogn)
public class 快速排序 {
public static void main(String[] args) {
int[] arr = { 5, 2, 7, 8, 9, 1 };
quickSort(arr, 0, arr.length-1);
System.out.println("排序后:" + Arrays.toString(arr));
}
//我们默认的是以每趟序列的第一个数为基准
public static void quickSort(int[] arr,int left,int right){
int temp = 0; // 基准
int L = left; // 初始化最左边的元素的索引
int r = right; // 初始化最右边的元素的索引
if(left<right){
//我们将这个数组中的第一个数赋值给temp。
temp = arr[left]; //temp相当于我们的基准,数列的第一个数
while(L!=r){ //我们假定,当L=r的时候退出该循环
//开始的时候,我们从最右边开始找元素,当我们找的元素的值大于等于我们的基准,那么我们将它向左移,直到这个元素的值小于基准为止。
while(r>L && arr[r]>=temp){
//出此循环时候r指针指向小于arr【1】的数字
--r;
}
//此时arr[r]已经小于temp,这个时候我们需要进行一个判断,看看是否还是符合L<r,如果符合的话,我们将arr[r]这个值,直接赋值到左边(数组的第一个值)并且让我们左边的l指向下一个位置。
if(L<r){
arr[L] = arr[r];//右边的拿到左边去
++L;//左指针继续往右边移动
}
//在右边换过来之后,我们需要交叉得变换,这个时候我们从左边开始比较
//前提还是l<r,只不过由于我们在左边开始,所以我们需要判断arr[L]如果小于temp的值,那么我们应该继续向右移,一直到大于等于temp的值为止。
while(L<r && arr[L]<temp){//出此循环时候,L指针执指向
++L;
}
此时arr[l]已经大于或者等于temp,这个时候我们需要进行一个判断,看看是否还是符合l<r,如果符合的话,我们将arr[l]这个值,直接赋值到右边(r所在的位置)并且让我们右边的r指向下一个位置。
if(L<r){
arr[r] = arr[L];//左边的拿到右边去
--r; //右指针继续往左边找
}
}
//在执行完上面的循环之后,我们除了基准的位置的空的外,基准左边的都小于它,基准右边的都大于它,形成了一个相对有序的序列,
//最终我们把之前赋值的temp加入到arr[L]中(因为这个时候r=L)
arr[L] = temp;
//执行完上面的依次循环之后,我们需要对子序列进行在依次的快速排序,执行过程与上面的相同。
quickSort(arr, left, L-1);
quickSort(arr, L+1, right);
}
}
}
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
首先考虑如何将二个有序数列合并。这个非常简单,只要比较两个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果其中一个数列为空,那直接将另一个数列的数据依次取出即可。
而归并排序的基本思路就是将数组依次分成2组A、B,如果这2组组内的数据都是有序的,那么就可以将这2组数据进行如上操作排序。那么如何让这2组数据有序呢?可以将A、B组各自再分成2组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组已经达到了有序,然后再合并相邻的2个小组就可以了。这样通过先递归分解数列,再合并数列就完成了归并排序。
归并排序是稳定的排序。最差时间复杂度为O(nlogn),平均时间复杂度为O(nlogn)。
public class 归并排序 {
//先分割,然后变成两个有序的数列,然后利用中转数组,之前的两个都在最左侧弄个指针,然后开始比较,谁小就谁放入中转并且指针后移一位
// 最后将中转数组的东西拷贝过来
public static void main(String[] args) {
int arr[]={8,4,5,7,1,3,6,2};
int temp[]=new int [arr.length];
bingsort(arr, 0, arr.length-1, temp);
System.out.println("排序后="+Arrays.toString(arr));
}
//分合方法
public static void bingsort(int[] arr,int left,int right , int[]temp) {
if (left<right){
int mid = (left+right)/2;
// 向左递归进行分解
bingsort(arr, left, mid, temp);
bingsort(arr, mid+1, right, temp);
//合并
bing(arr, left, mid, right, temp);
}
}
public static void bing(int[] arr,int left,int mid,int right , int[]temp) {
System.out.println("xxxxxxxx");
int i = left;//表示左边有序序列的初始索引
int j = mid +1;//右边的第一位索引
int t = 0;//指向temp数组的当前索引
// 1.先把左右两边的数据按规则填充到temp数组,直到左右两边的有序序列有一边处理完毕
while(i<=mid&&j<=right){
if(arr[i]<=arr[j]){
temp[t]= arr[i];
t+=1;
i+=1;
} else {
// 反之将右边的填充到temp数组
temp[t]=arr[j];
t+=1;
j+=1;
}
}
// 2.把有剩余的数据的一边的数据依次全部填充到temp
while (i<=mid) {
//左边还有剩余的,全部填充到temp
temp[t]= arr[i];
t+=1;
i+=1;
}
while (j<=right) {
//右边还有剩余的
temp[t]= arr[j];
t+=1;
j+=1;
}
// 3.temp数组 拷贝回去
// 并不是每次都拷贝8个
t=0;
int tempLeft=left;
System.out.println("templeft"+tempLeft+"right"+right+"left"+left);
while (tempLeft<=right ) {
arr[tempLeft]=temp[t];
t+=1;
tempLeft+=1;
}
}
}
【基数排序(Radix Sort)】
要讲基数排序,首先我们先来讲讲桶排序(Bucket Sort)。
假设我们有 N 个学生,他们的成绩是0到100之间的整数(于是最多有 M = 101 个不同的成绩),如何在线性时间内将学生的成绩排序输出?
我们可以用一个大小为101的数组a,a[i]存放成绩为i的学生人数,最后遍历输出即可。时间复杂度T(N, M) = O( M+N )。
那么当M>>N时,桶排序显然不再适用,那么基数排序就出现了。
基数排序属于"分配式排序"(distribution sort),又称"桶子法",顾名思义,它是通过元素的部分关键码,将要排序的元素分配至某些"桶"中,藉以达到排序的作用。
基数排序分为两类:第一类是最低位优先法,简称LSD法,即先从最低位开始排序,再对次低位进行排序,直到对最高位排序后得到一个有序序列。第二类是最高位优先法,简称MSD法,先从最高位开始排序,再对次高位进行排序,直到对最低位排序后得到一个有序序列。
当对给定序列进行次位优先(LSD)排序时,过程如下:
原序列: 64,8,216,512,27,729,0,1,343,125
桶子: 0 1 2 3 4 5 6 7 8 9
按个位: 0,1,512,343,64,125,216,27,8,729
按十位: 0,512,125 343 64
1,216, 27,
8 729
按百位: 0 125 216 343 512 729
1
8
27
64
最终序列:0 1 8 27 64 125 216 343 512 729
基数排序是稳定的排序方法。P为最大位数,N为数的数目,B为桶子的大小,最差时间复杂度为O(P(N+B)),平均时间复杂度为O(P(N+B))。
public class 基数排序 {
// 属于分配式排序,又称桶子法,他是通过键值得各个位的值
// 将要排序的元素分配至某些桶中,达到排序的作用
// 基数排序是桶排序的扩展,属于稳定性排序
//取出每个元素的个位数,然后看这个数一个放在哪个对应的桶,桶就是一个一维数组 (10个桶从0~9)
public static void main(String[] args) {
int arr[]={53,3,542,748,14,214};
radixSort(arr);
}
//定义一个二维数组来表示10个桶,每个桶就是一个一维数组
// 二维数组包含10个一维数组,为了防止放入数的时候数据溢出则每一个桶的大小都为arr.length
// 基数排序是使用空间换时间的经典算法
public static void radixSort(int []arr) {
//根据前面的推导过程 我们发现经过多少了取决于最大位数字
int max =arr[0];//假设第一位是最大的
for (int i = 0; i < arr.length; i++) {
if (arr[i]>max) {
max=arr[i];
}
}
//找到最大数后 去求是几位
int maxLength = (max+"").length();//max+一个空串变成字符串
int[][] bucket= new int[10][arr.length];
//定义一个一维数组来记录各个桶的每次放入的数据个数
// 例如bucketElementCount[0]就是记录0号桶放入数据的个数
int []bucketElementCount = new int[10];
for(int i=0,n=1;i<maxLength;i++,n*=10){
//每一轮根据情况处理不同位
for(int j =0;j<arr.length;j++){
int digitOfElement=arr[j]/n%10;
// 放入到对应的桶中
bucket[digitOfElement][bucketElementCount[digitOfElement]]=arr[j];
bucketElementCount[digitOfElement]++;
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入到原来的数组)
int index=0;
for(int k=0;k<bucket.length;k++){
//如果桶里有数据,我们就放入到原来的数组
if(bucketElementCount[k]!=0){
//循环该桶,即第K个一维数组
for (int l=0;l<bucketElementCount[k];l++){
//取出元素放到arr
arr[index++]=bucket[k][l];
}
}
bucketElementCount[k]=0;
}
System.out.println("第"+(i+1)+"轮对各位的排序处理"+Arrays.toString(arr));
}
}
}
堆排序
堆排序的算法是选择排序的优化版本。堆排序在寻找最小值(或最大值)的过程中使用了堆这种数据结构,提高了效率。堆是一个近似二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
算法一:
将需要排序的序列建堆,调整成最小堆。
每次删除并输出堆顶结点的值即最小值,再将最后一个数放置于堆顶结点的位置,最小堆的大小-1,向下调整成最小堆。重复上述步骤直到堆为空。
这样得到的输出序列即是升序。
算法二:
将需要排序的序列建堆,调整成最大堆。
每次把堆顶结点的值即最大值与最后一个结点交换位置,最大堆的大小-1,向下调整成最大堆。重复上述步骤直到结束。
最后层序遍历输出堆排序后的堆,即为升序序列。
#include <stdio.h>
#define maxn 105
int h[maxn];
int n;
//交换函数
void swap(int x,int y)
{
int t=h[x];
h[x]=h[y];
h[y]=t;
}
//向下比较调整成最大堆
void siftdown(int pos,int num)
{
int t,flag=0; //flag用来标记是否需要继续向下调整
while(!flag)
{
int t=pos; //用t记录父结点和左右儿子中值较大的结点编号
if(pos*2<=num&&h[t]<h[pos*2]) t=pos*2;
if(pos*2+1<=num&&h[t]<h[pos*2+1]) t=pos*2+1;
//如果最大的结点不是父结点
if(t!=pos)
{
swap(t,pos);
pos=t;
}
else flag=1;
}
}
void create()
{
//从最后一个非叶结点到第1个结点依次进行向下调整
for(int i=n/2;i>=1;i--)
siftdown(i,n);
}
//堆排序(升序)
void heapSort()
{
create(); //建堆
int num=n;
for(int i=n;i>1;i--)
{
swap(1,i); //交换最大值与最后一个数
num--;
siftdown(1,num); //前num个数调整成最大堆
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
heapSort();
for(int i=1;i<=n;i++) printf(i==n?"%d\n":"%d ",h[i]);
return 0;
}
堆排序是不稳定的排序。最差时间复杂度为O(nlogn),平均时间复杂度为O(nlogn)。
上述所有排序的动图和分析参考https://blog.csdn.net/qq_41117236/article/details/87903565八大排序:冒泡排序、插入排序、希尔排序、选择排序、堆排序、归并排序、快速排序、基数排序_菜鸡成长史-CSDN博客_八大排序八大排序算法详解(动图演示 思路分析 实例代码java 复杂度分析 适用场景) - 测试开发喵 - 博客园
那么我们要如何实现堆排序呢?下面以升序排序为例讲解。