40

数据结构之排序

 5 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/86509267?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本篇是数据结构与算法之美学习笔记

排序在在平时的开发中会经常用到,排序的算法也有很多,冒泡排序、插入排序、选择排序、归并排序,开旭排序、计数排序、基数排序、桶排序等。

如何分析一个排序算法

1.排序算法的执行效率

(1)最好情况、最坏情况、平均情况时间复杂度

为什么要区分三种时间复杂度呢?第一有些排序算法会区分,为了更好的对比,最好都做一下区分。第二,对于要排序的数据,有的接近有序,有的完全无序,有序度不同的数据对于排序的执行时间肯定是有影响的,我们需要知道排序算法在不同数据下的性能表现。

(2)时间复杂度的系数、常数、低阶

时间复杂度反应的是数据的规模n很大的时候的一个增长的趋势,所以它表示的时候会忽略系数、常数、低阶。但是实际的软件开发中,我们排序可能是10个、100个、1000个这种规模很小的数据,所以在对同一阶时间复杂度的排序算法性能对比的时候,我们需要把这些因素考虑进来

(3)比较次数和交换次数

基于比较的排序算法的执行过程,会涉及到两种操作,一种是元素比较大小,一种是元素的交换或移动。所以在分析执行效率的时候,应该把比较次数和交换次数都考虑进去

2.排序算法的内存消耗

算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。在排序算法中有个新的概念交原地排序

3.排序算法的稳定性

用执行效率和内存消耗来判断算法的好坏还不够,针对排序算法还有个指标是‘稳定性’。就是说,如果待排序的序列中存在值相等的元素,经过排序后,相等元素之间原有的先后顺序不变。

比如有一组数据 2,7,3,4,6,3 按照大小排序之后是:2,3,3,4,6,7

数组里有两个3,经过某种排序算法排序后,如果两个3的前后顺序没有改变,就把这种排序算法叫做稳定的排序算法;如果前后顺序发生了改变,这种排序算法就是不稳定的排序算法。

冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法,它只会操作相邻的两个数据,每次冒泡操作,都会对相邻的两个元素进行比较,看是否满足关系要求,如果不满足就让他们互换,一次冒泡,至少让一个元素移动到它应该在的位置,重复n次就完成了n个数据的排序工作。

描述:

比较相邻的元素,如果第一个比第二个大就交换

对每一个相邻的元素进行同样的工作

针对所有的元素,进行上面的步骤

比如:对一组数据 4,5,6,3,2,1,从小到大排序

初始状态:4,5,6,3,2,1

第一次冒泡:4,5,3,2,1,6

第二次冒泡:4,3,2,1,5,6

第三次冒泡:3,2,1,4,5,6

第四次冒泡:2,1,3,4,5,6

第五次冒泡:1,2,3,4,5,6

第六次冒泡:1,2,3,4,5,6

上面过程可以看到是可以优化一下,如果某次冒泡操作没有数据交换,说明已经完成排序不需要再继续执行后续的操作了。

代码:

public int[] bubbleSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++){
            //提前退出的标志
            boolean flag = false;
            for (int j = 0; j < array.length - 1 - i; j++){
                if (array[j + 1] < array[j]) {//交换
                    int temp = array[j + 1];
                    array[j + 1] = array[j];
                    array[j] = temp;
                    flag = true;//表示有了交换
                }
            }
                if(!flag) return array;//没交换了返回
        }
        return array;
    }
}

插入排序(Insertion Sort):

通过构建有序序列,对于未排序数据,在已排序序列中,从后往前扫描,找到相应位置并插入。

描述:

从第一个元素开始,这个可以认为已经排序

取出下一个元素,在已排序的元素序列中从后向前扫描

如果该元素大于新元素,将其移动到下一个位置

比如:跟上面一样对一组数据 4,5,6,3,2,1,从小到大排序

初始数据:4,5,6,3,2,1

第一次插入:4,5,6,3,2,1

第二次插入:4,5,6,3,2,1

第三次插入:4,5,6,3,2,1

第四次插入:3,4,5,6,2,1

第五次插入:2,3,4,5,6,1

第六次插入:1,2,3,4,5,6

代码:

public static int[] insertionSort(int[] array) {
        if (array.length == 0)
            return array;
        int current;
        for (int i = 0; i < array.length - 1; i++) {
            current = array[i + 1];
            int preIndex = i;
            while (preIndex >= 0 && current < array[preIndex]) {
                array[preIndex + 1] = array[preIndex];
                preIndex--;
            }
            array[preIndex + 1] = current;
        }
        return array;
    }

选择排序:(Selection Sort)

选择算法的实现思路跟插入排序类似,也有排序区间和未排序区间,但是它每次回从未排序区间中到最小的元素,将其放到已排序的区间末尾。

工作原理:首先在未排序系列中找到最小或最大的元素,存放到排序序列的起始位置,然后在从剩余的未排序的元素中寻找到最小或者最大的元素,然后放到已排序的序列的末尾,以此类推直到完毕。

比如:跟上面一样对一组数据 4,5,6,3,2,1,从小到大排序

初始数据:4,5,6,3,2,1

第一次选择:1,5,6,3,2,4

第二次选择:1,2,6,3,5,4

第三次选择:1,2,3,6,5,4

第四次选择:1,2,3,4,6,5

第五次选择:1,2,3,4,5,6

第六次选择:1,2,3,4,5,6

代码:

public int[] selectionSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) //找到最小的数
                    minIndex = j; //将最小数的索引保存
            }
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
        return array;
    }

选择排序是一种不稳定的排序算法,比如 5,8,5,2,9这样的一组数据,第一次找到的元素是2,与第一个5交换位置,这个时候,第一个5和第二个5的位置就发生了变化,所以就不稳定了。因此选择排序相比于插入和冒泡排序就稍微逊色了。

归并排序(Merge Sort):

归并排序的核心思想:如果要排序一个数组,先吧数组从中间分成前后两个部分,然后对前后两个部分分别排序,在将排序好的两部分合并在一起。

归并排序使用的是分治思想,将大的问题分解成小的子问题来解决,所以分治算法一般都是使用递归来实现的,‘分治是一种解决问题的思想,递归是一种编程技巧’。

比如:跟上面一样对一组数据 4,5,6,3,2,1,从小到大排序

原始数据 4,5,6,3,2,1

第一步分解 4,5,6 3,2,1

第二步分解 4,5 6 3,2 1

第三步分解 4 5 6 3 2 1

第四步合并 4,5 6 2,3 1

第五步合并 4,5,6 1,2,3

第六步合并 1,2,3,4,5,6

代码:

public int[] MergeSort(int[] array) {
        if (array.length < 2) return array;
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(MergeSort(left), MergeSort(right));
    }

    //合并操作
    public int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        for (int index = 0, i = 0, j = 0; index < result.length; index++) {
            if (i >= left.length)//左边数组比较完了
                result[index] = right[j++];
            else if (j >= right.length)//右边数组比较完了
                result[index] = left[i++];
            else if (left[i] > right[j])//谁小就把谁拿来放到result中,指针加1
                result[index] = right[j++];
            else////谁小就把谁拿来放到result中,指针加1
                result[index] = left[i++];
        }
        return result;
    }

快速排序(Quick Sort):

快速排序也是利用分治的思想。

(1)从数列中挑出一个元素称为基准(pivot)

(2)重新排列数列,比基准值小的摆放在基准前面,比基准值大的放在基准的后面,相同的可以放在任意一边。操作完成之后,基准就处于数组的中间位置。

(3)左边和右边递归执行同样的操作

比如:跟上面一样对一组数据 4,5,6,3,2,1,从小到大排序

原始数据 4,5,6,3,2,1

假设基准数据都选最右边的

第一步 1,5,6,3,2,4

第二步 1,3,2,4,6,5

第三步 1,2,3,4,6,5

第四步 1,2,3,4,5,6

代码:

// 快速排序递归函数,p,r为下标
  private void quickSortInternally(int[] a, int p, int r) {
    if (p >= r) return;
    int q = partition(a, p, r); // 获取分区点
    quickSortInternally(a, p, q-1);
    quickSortInternally(a, q+1, r);
  }
  private int partition(int[] a, int p, int r) {
    int pivot = a[r];
    int i = p;
    for(int j = p; j < r; ++j) {
      if (a[j] < pivot) {
        int tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
        ++i;
      }
    }
    //把基准点插入中间位置
    int tmp = a[i];
    a[i] = a[r];
    a[r] = tmp;
    return i;
  }

如何优化快速排序:

如果数据原来就是有序或者接近有序的,每次分区都选择最后一个数据,快速排序就会变的非常糟糕,出现这种情况主要就是我们选的分区不够合理

怎么选出比较好的分区呢?比较常用的方法有两种

1.三数取中法

从区间的首尾中分别取出一个数,然后对比大小,取这三个数的中间值所谓分区点,如果要排序的数据比较大,可以五数取中或者十数取中

2.随机法

每次从要排序的取件中,随机选择一个元素作为分区点,虽然不能保证每次都选的比较好,不过从概率论的角度看,每次分区都选的很差的情况也是很难遇到的,所以平均情况下还是比较好的。

桶排序(Bucket Sort)

核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据在单独排序,桶内排序之后,在把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

桶排序对数据的要求是非常苛刻的

首先要排序的数据根据需要很容易划分成m个桶,并且桶与桶之间有着天然的大小顺序,这样每个桶内的数据排序完之后,桶与桶之间的数据不需要在进行排序。

其次数据在各个桶之间的分部是比较均匀的,如果数据经过桶的划分之后,有些桶里的数据非常多,有的非常少不均匀,那桶内数据排序的时间复杂度就不是常量级的了。

桶排序比较适合用在外部排序中,所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。比如有10GB的订单数据,假设订单都是正数,我们希望按订单进行排序,但是内存有限只有几百兆,没办法一次性加载,这时候就可以使用桶排序。

代码:

public ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
    if (array == null || array.size() < 2)
      return array;
    int max = array.get(0), min = array.get(0);
    // 找到最大值最小值
    for (int i = 0; i < array.size(); i++) {
      if (array.get(i) > max)
        max = array.get(i);
      if (array.get(i) < min)
        min = array.get(i);
    }
    int bucketCount = (max - min) / bucketSize + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
    ArrayList<Integer> resultArr = new ArrayList<>();
    for (int i = 0; i < bucketCount; i++) {
      bucketArr.add(new ArrayList<Integer>());
    }
    for (int i = 0; i < array.size(); i++) {
      bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
    }
    for (int i = 0; i < bucketCount; i++) {
      if (bucketCount == 1)
        bucketSize--;
      ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
      for (int j = 0; j < temp.size(); j++)
        resultArr.add(temp.get(j));
    }
    return resultArr;
  }

计数排序(Counting Sort):

计数排序的核心在于将输入的数据转化为键存储在额外开辟的数组空间中。

计数排序是一种稳定的排序算法,计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中等值于i的元素个数,然后根据数组C来讲A中的元素排到正确的位置,它只能对整数进行排序

代码:

// 计数排序,a 是数组,n 是数组大小。假设数组中存储的都是非负整数。
public void countingSort(int[] a, int n) {
  if (n <= 1) return;

  // 查找数组中数据的范围
  int max = a[0];
  for (int i = 1; i < n; ++i) {
    if (max < a[i]) {
      max = a[i];
    }
  }

  int[] c = new int[max + 1]; // 申请一个计数数组 c,下标大小 [0,max]
  for (int i = 0; i <= max; ++i) {
    c[i] = 0;
  }

  // 计算每个元素的个数,放入 c 中
  for (int i = 0; i < n; ++i) {
    c[a[i]]++;
  }

  // 依次累加
  for (int i = 1; i <= max; ++i) {
    c[i] = c[i-1] + c[i];
  }

  // 临时数组 r,存储排序之后的结果
  int[] r = new int[n];
  // 计算排序的关键步骤,有点难理解
  for (int i = n - 1; i >= 0; --i) {
    int index = c[a[i]]-1;
    r[index] = a[i];
    c[a[i]]--;
  }

  // 将结果拷贝给 a 数组
  for (int i = 0; i < n; ++i) {
    a[i] = r[i];
  }
}

基数排序(Radix Sort)

按照低位先排序然后按照高位排序,直到最高位。

代码

public int[] RadixSort(int[] array) {
       if (array == null || array.length < 2)
           return array;
       // 1.先算出最大数的位数;
       int max = array[0];
       for (int i = 1; i < array.length; i++) {
           max = Math.max(max, array[i]);
       }
       int maxDigit = 0;
       while (max != 0) {
           max /= 10;
           maxDigit++;
       }
       int mod = 10, div = 1;
       ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
       for (int i = 0; i < 10; i++)
           bucketList.add(new ArrayList<Integer>());
       for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
           for (int j = 0; j < array.length; j++) {
               int num = (array[j] % mod) / div;
               bucketList.get(num).add(array[j]);
           }
           int index = 0;
           for (int j = 0; j < bucketList.size(); j++) {
               for (int k = 0; k < bucketList.get(j).size(); k++)
                   array[index++] = bucketList.get(j).get(k);
               bucketList.get(j).clear();
           }
       }
       return array;
   }

堆排序(Heap Sort)

用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

代码:

//声明全局变量,用于记录数组array的长度;
static int len;
   /**
    * 堆排序算法
    *
    * @param array
    * @return
    */
   public static int[] HeapSort(int[] array) {
       len = array.length;
       if (len < 1) return array;
       //1.构建一个最大堆
       buildMaxHeap(array);
       //2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
       while (len > 0) {
           swap(array, 0, len - 1);
           len--;
           adjustHeap(array, 0);
       }
       return array;
   }
   /**
    * 建立最大堆
    *
    * @param array
    */
   public static void buildMaxHeap(int[] array) {
       //从最后一个非叶子节点开始向上构造最大堆
       for (int i = (len - 1) / 2; i >= 0; i--) {
           adjustHeap(array, i);
       }
   }
   /**
    * 调整使之成为最大堆
    *
    * @param array
    * @param i
    */
   public static void adjustHeap(int[] array, int i) {
       int maxIndex = i;
       //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
       if (i * 2 < len && array[i * 2] > array[maxIndex])
           maxIndex = i * 2;
       //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
       if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
           maxIndex = i * 2 + 1;
       //如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
       if (maxIndex != i) {
           swap(array, maxIndex, i);
           adjustHeap(array, maxIndex);
       }
   }

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK