基本排序算法-java实现

来源:互联网 发布:saas软件服务协议 编辑:程序博客网 时间:2024/06/05 15:18

最近重新学习了排序算法,之前每次看完当时理解了,但是过一段时间就又忘了,尤其是代码,如果放一段时间有很多base case不知道怎么写了,所以还是应该详细的解读一下再不断了敲代码才能理解比较深刻。

1.冒泡排序(bubble sort)

冒泡排序是一种简单的排序算法。其基本思想是迭代地输出序列中的第一个到最后一个元素进行两两比较,当需要时交换这两个元素的位置。冒泡排序得名于键值小的元素如同气泡一样逐渐漂浮到序列的顶端。
1)时间复杂度 O(n*n)
2)空间复杂度O(1)
3 )稳定性:是

代码
private static void bubbleSort(int[] arr) {

  for (int i = arr.length - 1; i > 0; i--) {        for (int j = 0; j < i; j++) {            if (arr[i] < arr[j]) {                swap(arr, i, j);            }        }    }}

ps:https://visualgo.net/zh 这个网站可以看到动态的数据结构过程,所有排序方法可以输入实例来查看具体排序实现过程,推荐给大家。

2.选择排序(selection sort)

选择排序是一种原地排序(in-place)排序算法,适用于小文件,由于选择操作是基于键值的且交换操作只在需要时才执行,所以选择排序适用于数值较大和键值较小的文件。
1)优点:
-容易实现
-原地排序(不需要额外的储存空间)
2)缺点:
-扩展性较差
时间复杂度O(n*n)
3)算法:先寻找序列中的最小值,用当前位置交换最小值,重复上述过程直到整个序列排序完成。

代码:

public static void selectionSort(int[] arr) {

    for (int i = 0; i < arr.length - 1; i++) {        int minIndex = i;        for (int j = i + 1; j < arr.length; j++) {            minIndex = arr[j] < arr[minIndex] ? j : minIndex;        }        swap(arr, i, minIndex);    }}

3 .插入排序(insertion sort)

插入排序(insertion sort)是一种简单且有效的比较排序方法,在每次迭代过程中算法随机地在序列中一出一个元素,并将该元素插入待排序序列的正确位置。重复该过程,直到所有蒜素都被选择一次。
1)优点
-实现简单
-数据量少时效率高
-适应性(adaptive):如果输入的序列已经排序,则时间复杂度为O(n+d), d为反转的次数
-稳定性(stable):键值相同时它能够保持输入数据的原有次序
-原地(in-place):仅需要常量 O(1)的辅助内存空间
-即使(online):插入排序能够在接收序列的同时对其进行排序
2)算法插入算法主要是每个arr[i]和之前排好序的队列进行比较,并在合适的位置插入。

代码:
public static void insertionSort(int[] arr) {

    if (arr == null || arr.length < 2) {        return;    }    for (int i = 1; i < arr.length; i++) {        for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {            swap(arr, j, j + 1);        }    }}

example:
6 8 1 4 5 3 7 2-原序列
6 8 1 4 5 3 7 2(考虑索引位置0)
6 8 1 4 5 3 7 2 (考虑索引位置0-1)
1 6 8 4 5 3 7 2(考虑索引位置0-2,在6和8之间插入1)
1 4 6 8 5 3 7 2(重复上述过程直到序列排序完成)
1 4 5 6 8 3 7 2
1 3 4 5 6 7 8 2
1 2 3 4 5 6 7 8 (排序完成)

4)归并排序

对于我来说,排序算法难点就在于归并,快排和堆排序,因为这些排序方法比较复杂,而且牵扯到递归,而递归底层使用递归栈实现所以空间复杂度不好估计,而时间复杂度这三种排序方法都是O(nlogn),我记得MIT的算法公开课中都是用递归树来解决这些排序方法。

归并排序(merge sort)是分治的一个实例,其思想在于分(divide)和治(conquer)。首先将原始的待排序序列分开成若干个子序列,然后直到每个组都只有一个数值,然后合并过程,需要建立一个辅助空间进行存放,存放值为两个待排序子序列中较小的值,一直这样重复的递归下去,最后直到还剩两个子序列时,合并的结果就是最终的排好序的。
【例子】
原数组 65214387-divide
6521 4387-divide
65 21 43 87-divide
6 5 2 1 4 3 8 7-divide
56 12 34 78-conquer 比较大小并合并
1256 3478 -conquer 比较大小并合并
12345678-conquer 比较大小并合并

【例子2】
3 7 5 9
合并:
设置辅助数组:
3 (5比3大,3放入数组中)
3 5 (7比5大,5放入数组中)
3 5 7 (9比7大,7放入数组中)
3 5 7 9 (数列1结束,直接将数列2放入辅助数组末尾,即9放入数组中)
完成一次合并排序。
下一次,3 5 7 9 可以和 1 4 6 8进行合并排序。

private static void mergesort(int[] arr, int l, int r) {

    if (l == r)        return;    int mid = (l + r) / 2;    mergesort(arr, l, mid);    mergesort(arr, mid + 1, r);    merge(arr, l, mid, r);}private static void merge(int[] arr, int l, int mid, int r) {    int[] help = new int[r - l + 1];    int i = 0;    int p1 = l;    int p2 = mid + 1;    while (p1 <=mid && p2 <= r) {// 把较小的数先移到新数组中          help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];    }    while (p1 <= mid) { // 把左边剩余的数移入数组          help[i++] = arr[p1++];    }    while (p2 <= r) { // 把右边边剩余的数移入数组          help[i++] = arr[p2++];    }    for (int j = 0; j < help.length; j++) { // 把新数组中的数覆盖arr数组          arr[l + j] = help[j];    }}

注意:
1-判断条件(p1 <=mid && p2 <= r)一定要有等于,否则辅助数组会有0
2-辅助数组的大小为r - l + 1,然后传递给原序列大小为l + j

分析:
根据maste公式

时间复杂度为O(nlogn),其实也可以画一个递归树,然后求树的高度和叶子节点数。

5)快速排序(quick sort)

快速排序(quick sort)是分治算法技术中的一个实例,也称为分区交换排序。主要通过递归调用元素进行排序,是基于比较的排序方法。

划分:数组a[low..high]被分为两个非空子数组a[low…q]和a[q+1…high],使得a[low..q]中的每一个元素都小于等于a[q+1…high]中的元素,划分中需要索引q的位置。传统快排是一般选择数组最左边的元素为轴点(pivot),还有随机化快速排序是随机的选择轴点,这两种都只有一个最大区间或者最小区间,而改良版的快排序有最大和最小两个区间来排序和等于pivot的区间,这样序列待排序量减小,在工程上会大大减小时间。

过程:
1)如果数组中仅有一个元素或者没有元素需要被排序,返回。
2)选择数组中的一个元素作为枢轴点(pivot),通常最左边的元素为轴点。
3)把数组分为两部分—一部分元素小于pivot,一部分大于pivot。
4)对两部分递归调用

代码:

private static void quick(int[] arr, int low, int high) {    while (low < high) {        int[] pivot = partition(arr, low, high);        quick(arr, low, pivot[0] - 1);        quick(arr, pivot[1] + 1, high);    }}private static int[] partition(int[] arr, int l, int r) {    int less = l - 1;    int more = r;    while (l < more) {        if (arr[l] < arr[r]) {            swap(arr, ++less, l++);        } else if (arr[l] > arr[r]) {            swap(arr, --more, l);        } else {            l++;        }    }    swap(arr, more, r);    return new int[]{less + 1, more};}

注意:在java语言中Arrays.sort()函数对数组进行排序,如果是简单数组如1,2,3,4等底层进行快排,如果为复杂情况如对student进行排序则默认为归并排序,因为归并排序具有稳定性。

6)堆排序(heapsort)

我认为堆排序的难点在于有树的结构,所以对初学者来说很难使用,但是熟练以后,堆排序是非常神奇的排序过程,时间复杂度为O(nlogn),很多面试题需要排好序再进行操作时都会调用堆排序过程。

过程:
1.首先可将待排序数组变成一个树结构再变成大根堆,因为大根堆是一棵完全二叉树,所以树某一节点i的左孩子为2*i+1,右孩子为2*i+2,于是先迭代出大根堆。
2.然后有了大根堆下面进行下沉工作(heapify),过程为取堆的头节点,因为是大根堆,所以头节点为最大节点,与最后一个节点进行交换,然后序列长度减1,证明最大的树已经放到队尾,并减小了递归范围。
3.于是对现有的树进行变成大根堆的操作,直到变成了大根堆再重复上面的操作。
时间复杂度:O(Nlog(N))
额外空间复杂度:O(1)
是否可实现稳定性:否

代码:
public static void heapInsert(int[] arr, int index) {//建立大根堆

    while (arr[index] > arr[(index - 1) / 2]) {        swap(arr, index, (index - 1) / 2);        index = (index - 1) / 2;    }}public static void heapify(int[] arr, int index, int size) {//下沉操作    int left = index * 2 + 1;    while (left < size) {        int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;        largest = arr[largest] > arr[index] ? largest : index;        if (largest == index) {            break;        }        swap(arr, largest, index);        index = largest;        left = index * 2 + 1;    }}