高级排序

下面三个高级排序,评论时间复杂度能O(nlogn) 项目地址
普通排序
线性排序

// 交换数组中两个数 
swap (int[] data, int i, int j) {
	int temp = data[i];
  data[i] = data[j];
  data[j] = temp;
}
归并排序
public void sort (int[] data) {
  mergeSort(data, 0, data.length - 1);
}

private void mergeSort (int[] data, int low, int high) {
  if(low >= high)
    return;
  
  int mid = (low + high) / 2;
  mergeSort(data, low, mid);
  mergeSort(data, mid + 1, high);
  
  // if (data[mid] > data[mid + 1]) 这个判断可以对几乎有序的数据进行优化
  	merge(data, low, mid, high);
}

private void merge (int[] data, int low, int mid, int high) {
  int[] temp = new int[high - low + 1];
  int left = low;
  int right = mid + 1;
  int k = 0;
  while (left <= mid && right <= high) {
    if (data[left] <= data[right]) {
      temp[k] = data[left];
      left++;
    } else {
      temp[k] = data[right];
      right++;
    }
    k++;
  }
  while (left <= mid) {
    temp[k] = data[left];
    left++;
    k++;
  }
  while (right <= high) {
    temp[k] = data[right];
    right++;
    k++;
  }
  for (int i = 0; i < temp.length; i++) {
    data[low + i] = temp[i];
  }
}
  

原理:归并排序采用分治法,归并排序将一个数组不断二分,一直拆分为每个数组只有一个数,然后再将拆分后的子数组两两(也可能是二一)有序的合并,再合并成四个、八个、…有序的大数组,最后合并成一个有序的数组

merge方法可以看作是将两个数组合并成一个有序的数组,进入merge中的数组必定是可以分为两个已经有序的数组(如[1,2,3,4,1,2,3,4]或[5,3]),然后merge将这个数据处理为有序的。 具体过程大体如下,数组data为[1, 5, 8, 2, 3, 5],此时low=0,high=5,mid=2,可以看作将这个数组分为两个数组[1,5,8]和[2,3,5],具体是通过left和right两个指针来维护的。创建一个临时数组大小为数组大小6,第一个while循环依次比较两个数组的大小小的先放到temp中(1和2比1小将1放到temp,然后2和5比将2放到temp,然后3和5比将3放到temp,然后前面5和后面5比将前面5放到temp(先放后面5到temp则成了不稳定排序了),然后后面5和8比将5放到temp,此时有一个数组空了,循环结束了),循环完后temp为[1,2,3,5,5],此时后面的数组已经都放到temp中了,前面数组中还剩下一个数8,然后第二个while循环的作用是将前面数组剩下的数都放到temp中,将8也放到temp中了,第三个while循环的作用是将后面数组剩下的数都放到temp中,后面数组已经没数据了,此时temp中已经将data中数据都排好序放到temp中了,最后for循环再将temp中数组放回给data

mergeSort通过递归将数组拆分为小数组然后执行merge排序,然后再合并。数组data[5,1,8,5,3,2],通过递归执行mergeSort可以看作将data拆分为[5,1,8]和[5,3,2],然后再递归执行mergeSort左边分为[5,1]和[8],分别执行merge变为[1,5]和[8],然后再执行merge变为[1,5,8]。左边归并完实际数组变为[1,5,8,5,3,2]。然后右边分为[5,3]和[2],分别执行merge变为[3,5]和[2],然后再执行merge变为[2,3,5],最后两个大数组执行merge变为[1,2,3,5,5,8]。在merge之前也可以加上一个判断if (data[mid] > data[mid + 1]),这样已经有序的小数组就不需要再执行merge过程了,在有大量重复元素时比较有效。

快速排序
    public void sort(int[] data) {
        random = new Random();

        quickSort(data, 0, data.length - 1);
    }

    private void quickSort(int[] data, int left, int right) {
        if (left >= right) {
            return;
        }

        int p = partition(data, left, right);
        quickSort(data, left, p - 1);
        quickSort(data, p + 1, right);

    }

    /**
     * 对data[left...right]部分进行partition操作
     *
     * @return p  返回p(p的位置已经是正确的了),使得data[left...p-1] < data[p];         data[p+1...right] > data[p]
     */
    private int partition(int[] data, int left, int right) {
        return 0;
    }

原理:快速排序是冒泡排序的升级,他们都属于交换类排序,他们都是采用不段的比较和移动来实现排序的。快排采用分治思想,对于待排序数组,选择一个基准数,通常选第一个或最后一个,通过一趟扫描,将待排序数组分为两部分,一部分小于基准数,一部分大于基准数,此时基准数的位置正好是排好序的最终位置。然后对基准数分完的两部分数组采用递归再用同样的方式进行,直到数组中所有记录都有序为止。

partition(int[] data, int left, int right) 方法的作用是对 data[left…right] 部分进行partition操作,partition 操作就是在 data[left…right] 部分中找一个基准值 key,然后将 data[left…right] 部分分为大于 key 和小于 key 两部分,这样完成后 key 所在的位置 j 就是排好序的最终位置了,然后将 key 所在的位置 j 返回。

quickSort(int[] data, int left, int right) 方法传入待排序数组 data 和最大值最小值 0 和 data.length-1,然后调用 partition 方法,将数组分为两部分,并得到已经排好序的基准值在数组中的位置 p,然后再递归分别对 data[left,p-1]和 data[p, right] 再次进行 partition 抄作。递归终止条件是 left >= right,当所有递归都终止则数组有序。

过程:以一组数据大体看上面代码步骤,数组[5, 3, 8, 4, 6],5个数据,quickSort 中 left=0, right=4,left < right,第1次进行 partition 操作,对 data[0, 5] 全部数据进行partition 操作后的数组是 [4, 3, 5, 8, 6],此时5的位置即是最终位置,返回 5 此时在数组中的位置 2,然后 data[0, 1] 和 data [3, 4] 再递归执行 quickSort ;data[0, 1] 部分 left=0, right=1,left < right,然后进行第2次 partition 操作,对 data[0, 1] 段数据进行partition 操作后的数组是 [3, 4, 5, 8, 6],此时 4 的位置即是最终位置,返回 4 此时在数组中的位置 1,此时再进行递归 第一个 quickSort left 和 right 都等于0,第二个 quickSort left=2,right=1,则 left >= right ,这时第2部分递归结束;然后对 data [3, 4] 部分进行递归执行 quickSort ,data[3, 4] 部分 left=3, right=4,left < right,然后进行第3次 partition 操作,对 data[3, 4] 段数据进行partition 操作后的数组是 [3, 4, 5, 6, 8],此时8所在的位置即是最终位置,返回 8 在数组中的位置 4,此时再进行递归 left >= right,这时第3部分递归结束。到现在数组有序为 [3, 4, 5, 6, 8]。

partition 过程见下面:

partition_1

上面整个表格代表整个待排序数组段,整个待排序 data 段为 [left,right],取第一个值 key=data[left] 为基准数,然后遍历数组,小于 key 的数放在黄色部分,大于等于 key 的数放在绿色部分,E 为小于 key 中最后一个数,T 为大于等于 key 中第一个数,j 指向 E 的值,作为分界点,灰色部分为后面没有遍历过的数据,P 为没排序中的第一个值。

j 的初始值为 left,遍历数组,遇到第一个值 P 和 key 比较,若大于等于 key 什么都不需要做,直接 i 向后移动比较下一个值,若小于 key 则应该放到黄色区域,如何做到这一点呢,可以让 P 和蓝色中第一个值也就是 T 交换,交换后的 P 应该属于黄色区域,直接让 j 向后移动指向 P 即可,然后交换过去的 T 还属于绿色区域,然后 i 向后移动即可完成。把这个过程明白了,代码就好写了,如下:

/**
* 对data[left...right]部分进行partition操作
* @return p  返回p(p的位置已经是正确的了),使得data[left...p-1] < data[p]; data[p+1...right] > data[p]
*/
private int partition(int[] data, int left, int right) {
    int key = data[left];
    int j = left;
    int i = left + 1;

    while (i <= right) {
        if (data[i] >= key) {
            i++;
        } else {
            swap(data, j + 1, i);
            j++;
            i++;
        }
    }

    swap(data, j, left);

    return j;
}

上面的 partition 过程会将等于 key 的数据都放到绿色的部分,也可以放到黄色的部分,但是有一个问题就是当数组中存在大量相同数据时这样每次通过 partition 分来的两部分数据会出现一部分就几个数据,一部分数据非常多,这样通过 partition 分成的递归树平衡度非常差,基本退化为链表,成了O(n*n)级别的排序了,下面优化一下 partition 过程,避免这个问题。

partition_2

上面整个表格代表整个待排序数组段,整个待排序 data 段为 [left,right],取第一个值 key=data[left] 为基准数,i 重前向后,j 重后往前,然后遍历数组,小于 key 的数放在黄色部分,大于 key 的数放在绿色部分,中间灰色部分为没有遍历过的数据,E 为没有遍历数左边第一个数,T 为没有遍历数又边第一个数,i 指向E,j 指向 T。

i 的初始值为 left+1,j 的初始值为 right,分别从前后两个方向遍历数组,左边遇到第一个值 E 和 key 比较,若小于 key 什么都不需要做,直接 i 向后移动比较下一个值,若 E 大于等于 key,循环结束;右边遇到第一个值 T 和 key 比较,若大于 key 什么都不需要做,直接 j 向前移动比较下一个值,若 T 小于等于 key,循环结束;此时若 i 已经在 j 的右边就完成了 partition 过程,否则交换 i 和 j 的值,交换后 T 正好属于黄色区域,E 正好属于绿色区域,i 向后移动,j 向前移动,然后继续这个过程,直到 i > j,最后循环结束,最后将 key 放到 j 的位置就完成了。partition2 过程和 partition 相比,主要的区别是将等于 key 的部分,分到了左右两边了,不会出现一部分数据太多的情况。

private int partition2(int[] data, int left, int right) {
    int key = data[left]; // 取第一个数为基准数
    int i = left + 1, j = right;

    while (true) {
        while (i <= right && data[i] < key) i++;
        while (j >= left + 1 && data[j] > key) j--;
        if (i > j) break;

        swap(data, i, j);

        i++;
        j--;
    }

    // 交换left和j的位置,交换后data[j]的值已经是正确的位置了
    swap(data, left, j);

    return j;

}
堆排序

原理:主要是利用大顶堆(或小顶堆)的性质,对数组进行变换实现排序的。
big_heap_0

在堆排序前先了解一些基本概念。二叉堆是每个节点最多有两个子节点;二叉堆是一棵完全二叉树;二叉堆中取出任何一部分都是一个二叉堆;二叉堆分为大顶堆和小顶堆。

  • 大顶堆:父结点的键值总是大于或等于任何一个子节点的键值
  • 最小堆:父结点的键值总是小于或等于任何一个子节点的键值

下面通过构建一个大顶堆实现排序(小顶堆也一样),因为二叉堆是个完全二叉树,故可以通过数组来实现二叉堆。

上面图片是一个大顶堆,可以看出每个父节点都不小于子节点,但需要注意的是相连的子节点,比如第9个节点的值是30,是大于上面第3个节点的值20的,这是符合大顶堆的定义的。

下面介绍一下 shiftDown 操作

    // arr数组中以k为父节点,n为结尾这段大顶堆(k和n都为数组下标)
    private void shiftDown(int[] arr, int n, int k) {
        while (2 * k + 1 < n) {
            int j = 2 * k + 1; // 左子节点
            if (j + 1 < n && arr[j + 1] > arr[j]) {
                j += 1;
            }

            if (arr[k] >= arr[j]) {
                break;
            }

            swap(arr, k, j);
            k = j;

        }
    }

shiftDown 意为下浮,主要功能是一个二叉堆中的父节点不满足二叉堆的性质,则通过下浮操作重新变为一个二叉堆。比如上面图片中的第一个节点45变为12,则以12为父节点的这棵树不满足二叉堆的性质,因为12比下面38小了,此时通过 shiftDown 操作让以12为头的树变为一个二叉堆,具体逻辑是12跟子节点比,有两个子节点20和38,则跟最大的子节点交换,交换完38上去比左边子节点20大了,左边满足二叉堆的性质了,右边换完12继续跟他的子节点比较,12只有一个子节点30了,比30小,则将30换上来,则下浮操作完成,就变成一个二叉堆了。

2*k+1 表示的是 节点 k 的左子节点,2k+1<n 则表示节点 k 有子节点(有左孩子),2k+2<n 则表示节点 k 有右子节点,然后找出左右孩子中值大的下标赋值给 j,然后 j 和 节点 k 比较,父节点 k 的值不小于子节点 j 的值则说明本来就是个大顶堆,循环结束。否则将父节点 k 的值和子节点 j 的值交换,并将 k 当作父节点(k=j)继续看 k 下面的子树,直到子树都否符合大顶堆的性质。

下面是堆排序代码

    public void sort(int[] data) {
        heapSort(data, data.length);
    }

    private void heapSort(int[] arr, int n) {
        // heapify(构建堆)
        for (int i = (n - 1) / 2; i >= 0; i--) {
            shiftDown(arr, n, i);
        }

        // sort
        for (int i = n - 1; i > 0; i--) {
            swap(arr, 0, i);
            shiftDown(arr, i, 0);
        }
    }

构建堆:首先将传来的数组构建成一个大顶堆,有个经典的构建堆的方式叫作 heapify,就是数组从后向前都执行 shiftDown,比如上面的图片5,6,7,8,9都是叶子节点,本身已经符合二叉堆的性质了,所以可以从有子节点的节点开始,完全二叉树的最后一个非叶子节点的下标为 (n-1)/2,n 为数组大小,所以从下标为4的节点开始直到3,2,1,0,分别把自己当做一棵树,都执行 shiftDown 将自己变为一个二叉堆,当下标为0的节点完成后整个树就是一个大顶堆了。

排序:大顶堆的第0个值是最大值,可以将第0个值和最后一个值交换,交换完后最后一个值即为最大值了,然后将数组最后一个值抛开不管,继续看前面 n-1 个值,之前最后一个值换到第0个位置了,此时这 n-1 个值不再符合大顶堆的性质了,第1~n-2这 n-2 个值还是满足二叉堆的性质的,所以此时让第0个值执行一次 shiftDown,完成后第0个值又为这 0~n-2 这 n-1 个值中的最大值,又满足大顶堆的性质了,然后继续让第0个值和第 n-2 个值交换,交换完后倒数第二个值是第二大的值了,然后再执行 shiftDown,一直重复这个过程,当最后只剩下一个值时就都有序了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值