[算法]快速排序,归并排序,堆排序的数组和单链表实现

这三个排序的时间复杂度都是O(nlogn),因此这里放到一块儿说。html

1. 快速排序

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),经过一趟排序将要排序的数据分割成独立的两部分,其中一部分的全部数据都比另一部分的全部数据都要小,而后再按此方法对这两部分数据分别进行快速排序,整个排序过程能够递归进行,以此达到整个数据变成有序序列。node

步骤为:算法

  1. 从数列中挑出一个元素,称为"基准"(pivot),
  2. 从新排序数列,全部元素比基准值小的摆放在基准前面,全部元素比基准值大的摆在基准的后面(相同的数能够到任一边)。在这个分区结束以后,该基准就处于数列的中间位置。这个称为分区(partition)操做。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,可是这个算法总会结束,由于在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。数组

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n2)
  • 稳定性:不稳定

从一开始快速排序平均须要花费O(n log n)时间的描述并不明显。可是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用O(n)的时间。在使用结合(concatenation)的版本中,这项运算也是O(n)。数据结构

在最好的状况,每次咱们运行一次分区,咱们会把一个数列分为两个几近相等的片断。这个意思就是每次递归调用处理一半大小的数列。所以,在到达大小为一的数列前,咱们只要做log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。可是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;所以,程序调用的每一层次结构总共所有仅须要O(n)的时间(每一个调用有某些共同的额外耗费,可是由于在每一层次结构仅仅只有O(n)个调用,这些被概括在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。ide

数组实现

public class QuickSort { public static void main(String[] args) { int[] a = { 1, 2, 4, 5, 7, 4, 5, 3, 9, 0 }; quickSort(a, 0, a.length - 1); System.out.println(Arrays.toString(a)); } private static void quickSort(int[] a, int low, int high) { if(low >= high){ return; } int cur1 = low; int cur2 = high; int temp = a[low]; while(cur1 < cur2){ while(cur1 < cur2 && a[cur2] > temp){ cur2--; } a[cur1] = a[cur2]; while(cur1 < cur2 && a[cur1] <= temp){ cur1++; } a[cur2] = a[cur1]; } a[cur1] = temp; quickSort(a, low, cur1 - 1); quickSort(a, cur1 + 1, high); } }

单链表实现

在通常实现的快速排序中,咱们经过首尾指针来对元素进行切分,下面采用快排的另外一种方法来对元素进行切分。不然的话,单链表快排不方便,由于没有索引,很差从后往前遍历。测试

咱们只须要两个指针p1和p2,这两个指针均往next方向移动,移动的过程当中保持p1以前的key都小于选定的key,p1和p2之间的key都大于选定的key,那么当p2走到末尾时交换p1与key值便完成了一次切分。ui

图示以下:
spa

/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */
class QuickSortList{ public ListNode sortList(ListNode head) { //采用快速排序
       quickSort(head, null); return head; } public static void quickSort(ListNode head, ListNode end) { if(head == end){ return; } ListNode p1 = head, p2 = head.next; //走到末尾才停
        while (p2 != end) { //大于key值时,p1向前走一步,交换p1与p2的值
            if (p2.val < head.val) { p1 = p1.next; int temp = p1.val; p1.val = p2.val; p2.val = temp; } p2 = p2.next; } //当有序时,不交换p1和key值
        if (p1 != head) { int temp = p1.val; p1.val = head.val; head.val = temp; } quickSort(head, p1); quickSort(p1.next, end); } }

能够在https://leetcode-cn.com/problems/sort-list/description/进行测试。 设计

2. 归并排序

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题而后递归求解,而治(conquer)的阶段则将分的阶段获得的各答案"修补"在一块儿,即分而治之)。

分而治之

能够看到这种结构很像一棵彻底二叉树,本文的归并排序咱们采用递归去实现(也可采用迭代的方式去实现)。阶段能够理解为就是递归拆分子序列的过程,递归深度为log2n。

再来看看阶段,咱们须要将两个已经有序的子序列合并成一个有序序列,好比上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

数组实现

public class MergeSort { public static void main(String[] args) { int[] arr = { 9, 8, 7, 6, 5, 4, 3, 2, 1 }; sort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); } public static void sort(int[] arr, int left, int right) { if(left < right){ int middle = (left + right) / 2; sort(arr, left, middle);//对左子序列排序
            sort(arr, middle + 1, right);//对右子序列排序
 merge(arr, left, right, middle); } } private static void merge(int[] arr, int left, int right, int middle) { int[] temp = new int[arr.length]; int i = left;//左指针
        int j = middle + 1;//右指针
        int k = i;//这是temp的指针
        while(i <= middle && j <= right){ if(arr[i] < arr[j]){ temp[k++] = arr[i++]; }else{ temp[k++] = arr[j++]; } } //处理剩余的字符
        while(i <= middle){ temp[k++] = arr[i++]; } while(j <= right){ temp[k++] = arr[j++]; } // 将临时数组中的内容存储到原数组中
        while (left <= right) { arr[left] = temp[left++]; } } }

单链表实现

归并排序应该算是链表排序最佳的选择了,保证了最好和最坏时间复杂度都是nlogn,并且它在数组排序中广受诟病的空间复杂度在链表排序中也从O(n)降到了O(1)。

归并排序的通常步骤为:

  1. 将待排序数组(链表)取中点并一分为二;
  2. 递归地对左半部分进行归并排序;
  3. 递归地对右半部分进行归并排序;
  4. 将两个半部分进行合并(merge),获得结果。

首先用快慢指针(快慢指针思路,快指针一次走两步,慢指针一次走一步,快指针在链表末尾时,慢指针刚好在链表中点)的方法找到链表中间节点,而后递归的对两个子链表排序,把两个排好序的子链表合并成一条有序的链表。

/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */
class MergeSortList{ public ListNode sortList(ListNode head) { if(head == null || head.next == null){ return head; } ListNode mid = getMid(head); ListNode right = mid.next; mid.next = null;//将两个链表分开
        ListNode node = merge(sortList(head), sortList(right)); return node; } /** * 获取链表的中间结点,偶数时取中间第一个 * @param head * @return
     */
    public ListNode getMid(ListNode head){ if(head == null){ return head; } ListNode fast = head;//快指针
        ListNode slow = head;//慢指针
        
        while(fast.next != null && fast.next.next != null){ slow = slow.next; fast = fast.next.next; } return slow; } /** * 归并两个有序的链表 * 把另外一个链表插入到当前链表中 * @param head1 * @param head2 * @return
     */
    private ListNode merge(ListNode head1, ListNode head2){ if(head1 == null || head2 == null){ return head1 != null ? head1 : head2; } ListNode head = head1.val < head2.val ? head1 : head2; ListNode cur1 = head == head1 ? head1 : head2; ListNode cur2 = head == head1 ? head2 : head1; ListNode pre = null;//用来记录cur1的上一个
        ListNode next = null;//用来记录cur2的下一个
        while(cur1 != null && cur2 != null){ if(cur1.val <= cur2.val){//这里必定要有=,不然一旦cur1的value和cur2的value相等的话,下面的pre.next会出现空指针异常
                pre = cur1; cur1 = cur1.next; }else{ next = cur2.next; pre.next = cur2; cur2.next = cur1; pre = cur2; cur2 = next; } } pre.next = cur1 == null ? cur2 : cur1; return head; } }

能够在https://leetcode-cn.com/problems/sort-list/description/进行测试。 

归并排序还能够不用递归,具体参考博客:http://www.cnblogs.com/weiyinfu/p/8546080.html

3. 堆排序

  堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

  堆是具备如下性质的彻底二叉树:每一个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每一个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。以下图:

同时,咱们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

该数组从逻辑上讲就是一个堆结构,咱们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  

ok,了解了这些定义。接下来,咱们来看看堆排序的基本思想及基本步骤:

堆排序的基本思想是:将待排序序列构形成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。而后将剩余n-1个元素从新构形成一个堆,这样会获得n个元素的次小值。如此反复执行,便能获得一个有序序列了。

步骤一 构造初始堆。将给定无序序列构形成一个大顶堆(通常升序采用大顶堆,降序采用小顶堆)。

假设给定无序序列结构以下

此时咱们从最后一个非叶子结点开始(叶结点天然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

找到第二个非叶节点4,因为[4,9,8]中9元素最大,4和9交换。

这时,交换致使了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,咱们就将一个无需序列构形成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。而后继续调整堆,再将堆顶元素与末尾元素交换,获得第二大元素。如此反复进行交换、重建、交换。

将堆顶元素9和末尾元素4进行交换。

从新调整结构,使其继续知足堆定义。

再将堆顶元素8与末尾元素5进行交换,获得第二大元素8。

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

再简单总结下堆排序的基本思路:

  a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  c.从新调整结构,使其知足堆定义,而后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

数组实现

public class HeapSort { public static void main(String[] args) { int[] arr = new int[] { 7, 8, 5, 9, 4, 6, 2, 1, 3 }; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int[] arr) { //1.先肯定大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) { adjustHeap(i, arr, arr.length); } //2.交换并取出
        for (int j = arr.length - 1; j > 0; j--) { int temp = arr[j]; arr[j] = arr[0]; arr[0] = temp; adjustHeap(0, arr, j); } } private static void adjustHeap(int i, int[] arr, int length) { int temp = arr[i]; for (int k = 2 * i + 1; k < length; k = 2 * k + 1) { if (k + 1 < length && arr[k] < arr[k + 1]) {//选取两个叶子节点中较大的那一个
                k++; } if (arr[k] > temp) { arr[i] = arr[k]; i = k; } } arr[i] = temp; } }

单链表实现

暂时没有思路,欢迎补充交流。

参考文献

http://www.javashuo.com/article/p-klzzbtwv-p.html

http://www.cnblogs.com/chengxiao/p/6194356.html

http://www.cnblogs.com/chengxiao/p/6129630.html

http://www.javashuo.com/article/p-xhwasnvx-t.html

相关文章
相关标签/搜索