时间复杂度为O(nlogn)的排序(JAVA)

时间复杂度为O(nlogn)的排序

归并排序

思路

一个数组,前后一分为二,只要把两个数组都排好序,最后将两个数组合并到一起,就完成了整个数组的排序。
这种解决问题的思想叫“分治”,解决一个大问题的途径是通过解决若干个小问题来完成的。
举个例子,有数组[4,6,2,8,1]
前后一分为二:[4,6,2][8,1]
需要分别完成上述两个数组的排序后,最终合并两数组
完成[4,6,2]的排序是接着一分为二:[4,6][2],…
完成[4,6]的排序是接着一分为二:[4][6],到此处无法再分了,就进行合并操作
于是[4][6]合并为了[4,6],再和[2]合并为[2,4,6]
[8,1]那边通过一系列操作得到的是[1,8]
最终[2,4,6][1,8]合并得到[1,2,4,6,8],完成排序
时间复杂度为O(nlogn)的排序(JAVA)_第1张图片
这里用到了递归调用

写递归代码的过程

递归方法中要完成下边几件事
第一,判断要排序的数组长度是否大于1,如果为1或为空,直接返回
第二,解决数组左半部分的排序(调用本递归方法)
第三,解决数组右半部分的排序(调用本递归方法)
第四,合并两部分有序数组为一个数组,完成排序
先放一下最早写的代码

import java.util.Arrays;

import org.junit.Test;

public class MergeSort {

    public int[] mergeSort(int[] array, int length) {
        if (length <= 1) {
            return array;
        }
        int mid = (length - 1) / 2;
        // copyOfRange方法的from to是左闭右开的,所以left获取的是index从0到mid的,right获取到的是index从mid+1到length-1的
        int[] left = Arrays.copyOfRange(array, 0, mid + 1);
        int[] right = Arrays.copyOfRange(array, mid + 1, length);
        return merge(mergeSort(left, left.length), mergeSort(right, right.length));
    }

    private int[] merge(int[] left, int[] right) {
        if (left == null || left.length == 0) {
            return right;
        }
        if (right == null || right.length == 0) {
            return left;
        }
        int[] temp = new int[left.length + right.length];
        int leftPos = 0;
        int rightPos = 0;
        int tempPos = 0;
        while (leftPos < left.length && rightPos < right.length) {
            if (left[leftPos] < right[rightPos]) {
                temp[tempPos++] = left[leftPos++];
            } else {
                temp[tempPos++] = right[rightPos++];
            }
        }
        if (leftPos == left.length) {
            while (rightPos < right.length) {
                temp[tempPos++] = right[rightPos++];
            }
        } else {
            while (leftPos < left.length) {
                temp[tempPos++] = left[leftPos++];
            }
        }
        return temp;
    }

    @Test
    public void test() {
        int[] array1 = {6, 9, 1, 5, 9, 4, 2};
        System.out.println(Arrays.toString(mergeSort(array1, array1.length)));
        int[] array2 = {6, 9, 1, 5, 9, 4, 2, 7};
        System.out.println(Arrays.toString(mergeSort(array2, array2.length)));
    }

}

在递归方法mergeSort中完成了上述四件事。但每次调用两次Arrays.copyOfRange生成新数组,有些浪费空间,可以使用传递索引范围来确定要处理的数组(类似从index=0index=length-1),写一个重载的方法mergeSort,下边是伪代码

// 原mergeSort入参中有数组长度,这里left和right相应的就是left=0,right=length-1
public void mergeSort(int[] array, int left, int right) {
	// 对应于length<=1时return,相当于length-1<=0,也就是right<=left时return
	if(left >= right) {
		return;
	}
	// 确定一个中间索引mid
	int mid = (left + right) / 2;
	// 这里有种更装逼的写法,当left和right都足够大的时候,相加可能超出int最大范围,这里最好改写为
	mid = left + (right - left) / 2;
	// 如新的重载方法中描述,right指定的索引是有效的,那左半部分的数组索引就确定了,从left到mid;右半部分的数组索引就是,从mid+1到right
	mergeSort(array, left, mid);
	mergeSort(array, mid+1, right);
	// 最后一步合并,原merge是传两个数组进去,这时候也可以传索引来完成了,原merge方法中的入参left数组用array和首尾index替代,入参right数组用array和首尾index替代,
	// 比如merge(int[] array, int leftStart, int leftEnd, int rightStart, int rightEnd);
	// 其实leftEnd + 1 = rightStart,所以可以省略一个入参,最终方法的签名为merge(int[] array, int left, int mid, int right);
	// 另外,不用返回一个数组对象,可直接把新生成的数据对象按left和right放回到array中即可
	merge(array, left, mid, right);
}

既然已经重新定义了merge方法的签名,merge就需要重写一下了,但原功能保持不变,下边是伪代码

private void merge(int[] array, int left, int mid, int right) {
	// 这里不需要判断left和mid和right的关系。能进入merge,说明left比right小,原merge中判断length=0的情况,在这里不存在,因为left<=mid<=right,这就保证了进来的两个数组肯定满足length>=1
    // 从left到right的左闭右闭区间长度是right-left+1,创建一个临时数组(之前merge方法的返回值),长度是right-left+1
    int[] temp = new int[right-left+1];
    // 类似之前的left数组从0开始,这里要从该元素在array中的索引left开始了
    int leftPos = left;
    // right部分从mid+1开始(这里省略了一个入参rightStart,其实就是leftEnd加上1即mid+1)
    int rightPos = mid+1;
    // 新数组中的索引从0开始
    int tempPos = 0;
    while (leftPos <= mid && rightPos <= right) {
        if (array[leftPos] < array[rightPos]) {
        	// 左边更小,取左边值存入,左边索引++
            temp[tempPos++] = array[leftPos++];
        } else {
        	// 右边更小,取右边值存入,右边索引++
            temp[tempPos++] = array[rightPos++];
        }
    }
    // 处理尾部
    if (leftPos > mid) {
    	// 左侧先移动完,将右侧依次存入
        while (rightPos <= right) {
            temp[tempPos++] = array[rightPos++];
        }
    } else {
    	// 右侧先移动完,将左侧依次存入
        while (leftPos <= mid) {
            temp[tempPos++] = array[leftPos++];
        }
    }
    // 最后将temp放回到array中
    for (int i = 0; i < temp.length; i++) {
    	array[left+i] = temp[i];
    }
}

最终代码

public class MergeSort {

    public void mergeSort(int[] array, int left, int right) {
        if (left >= right) {
            return;
        }
        int mid = left + (right - left) / 2;
        mergeSort(array, left, mid);
        mergeSort(array, mid + 1, right);
        merge(array, left, mid, right);
    }

    private void merge(int[] array, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int leftPos = left;
        int rightPos = mid + 1;
        int tempPos = 0;
        while (leftPos <= mid && rightPos <= right) {
            if (array[leftPos] <= array[rightPos]) {
                temp[tempPos++] = array[leftPos++];
            } else {
                temp[tempPos++] = array[rightPos++];
            }
        }
        if (leftPos > mid) {
            while (rightPos <= right) {
                temp[tempPos++] = array[rightPos++];
            }
        } else {
            while (leftPos <= mid) {
                temp[tempPos++] = array[leftPos++];
            }
        }
        for (int i = 0; i < temp.length; i++) {
            array[i + left] = temp[i];
        }
    }

    @Test
    public void test() {
        int[] array1 = {6, 9, 1, 5, 9, 4, 2};
        System.out.println(Arrays.toString(array1));
        mergeSort(array1, 0, array1.length - 1);
        System.out.println(Arrays.toString(array1));
        int[] array2 = {6, 9, 1, 5, 9, 4, 2, 7};
        System.out.println(Arrays.toString(array2));
        mergeSort(array2, 0, array2.length - 1);
        System.out.println(Arrays.toString(array2));
    }

}

merge方法中,定义temp时还可以直接定义一个和array等长的数组(不建议这么干,详见后边tips)
代码如下:

private void merge2(int[] array, int left, int mid, int right) {
    int[] temp = new int[array.length];
    int leftPos = left;
    int rightPos = mid + 1;
    int tempPos = left;
    while (leftPos <= mid && rightPos <= right) {
        if (array[leftPos] <= array[rightPos]) {
            temp[tempPos++] = array[leftPos++];
        } else {
            temp[tempPos++] = array[rightPos++];
        }
    }
    if (leftPos > mid) {
        while (rightPos <= right) {
            temp[tempPos++] = array[rightPos++];
        }
    } else {
        while (leftPos <= mid) {
            temp[tempPos++] = array[leftPos++];
        }
    }
    for (int i = left; i <= right; i++) {
        array[i] = temp[i];
    }
}

tips:这里是后来加进去的
后边做性能测试,用了temp = new int[array.length]这种方式,巨慢无比,一度以为归并排序并不快,sorry,详情可见排序性能测试(JAVA),所以这种方案还是不要用了

归并排序的非递归写法

public void mergeSort(int[] array) {
    // 每次处理的元素个数 size = 1 2 4 8 ...
    for (int size = 1; size < array.length; size += size) {
        // 处理相邻两个长度为size的子数组的合并
        // 如果第二个子数组不存在,就可以省略合并过程了,这就是 i+size < array.length 的意义
        for (int i = 0; i + size < array.length; i += size + size) {
            // 相邻两个长度为size的子数组,开始索引分别是
            // a: i -> i+size-1
            // b: i+size -> i+size+size-1
            // 此时理论上b数组的尾索引是会超出边界的
            int left = i;
            int mid = i + size - 1;
            int right = i + size + size - 1;
            if (right > array.length - 1) {
                right = array.length - 1;
            }
            merge(array, left, mid, right);
        }
    }
}

快速排序

一个数组,随便找一个元素作为基准(分隔元素),比它小的放在前边,比它大的放在后边。这时候数组被分区为三部分。接着只要左半部分数组和右半部分数组都有序了,整个数组就有序了。
这种解决问题的思想用到的也是“分治”。但和归并排序不同,归并排序是先解决子数组的排序,最后再合并;快速排序是先分区,再解决子数组的排序。
举个例子,有数组[4,6,2,8,1,5]
找一个元素5作为分隔元素(一般取数组最后一位做分隔元素),比5小的放前边,比5大的放后边
于是分区得到[4,2,1] 5 [6,8]
接着对[4,2,1]做分区,找个分隔元素1,就得到了[] 1 [4,2]
再对[4,2]分区,取分隔元素2,就得到了[] 2 [4],子数组元素个数为1,结束
另外由[6,8]分区得到了[6] 8 []
整理一下变化的过程如下

[4,6,2,8,1,5]
[4,2,1] - 5 - [6,8]
[] - 1 - [4,2] - 5 - [6] - 8 - []
[] - 1 - [] - 2 - [4] - 5 - [6] - 8 - []
[1,2,4,5,6,8]

这里用到的还是递归调用

写递归代码的过程

递归方法中要完成下边几件事
第一,判断要排序的数组长度是否大于1,如果为1或为空,直接返回
第二,分区操作,找一个分隔元素,一般取index=length-1,并遍历数组元素与分隔元素比较,小的放前边,大的放后边
第三,解决左半部分的排序问题(调用递归方法)
第四,解决右半部分的排序问题(调用递归方法)
先放一下最早写的代码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Test;

public class QuickSort {

    public void quickSort(int[] array, int length) {
    	// 1
        if (length <= 1) {
            return;
        }
        // 2
        int partition = array[length - 1];
        List<Integer> leftList = new ArrayList<>();
        List<Integer> rightList = new ArrayList<>();
        for (int i = 0; i < array.length - 1; i++) {
            int item = array[i];
            if (item > partition) {
                rightList.add(item);
            } else {
                leftList.add(item);
            }
        }

        int[] left = new int[leftList.size()];
        for (int i = 0; i < leftList.size(); i++) {
            left[i] = leftList.get(i);
        }

        int[] right = new int[rightList.size()];
        for (int i = 0; i < rightList.size(); i++) {
            right[i] = rightList.get(i);
        }
		
		// 3
        quickSort(left, left.length);
        // 4
        quickSort(right, right.length);

        int i = 0;
        for (; i < left.length; i++) {
            array[i] = left[i];
        }
        array[i++] = partition;
        for (int j = 0; j < right.length; j++) {
            array[i + j] = right[j];
        }
    }

    @Test
    public void test() {
        int[] array1 = {2, 6, 8, 1, 5, 3};
        System.out.println(Arrays.toString(array1));
        quickSort(array1, array1.length);
        System.out.println(Arrays.toString(array1));

        int[] array2 = {2, 6, 8, 1, 5, 3, 4};
        System.out.println(Arrays.toString(array2));
        quickSort(array2, array2.length);
        System.out.println(Arrays.toString(array2));
    }

}

quickSort方法中执行了上述四个步骤,最终加了一步,将元素回写到array数组中
和归并排序一样,考虑使用索引,每次递归中操作的是一个区间内的数组,而分区可以抽出一个方法,只需要告诉调用者,分隔元素的索引位置,那么之前和之后的数组需要递归排序就可以了。下边是伪代码

public void quickSort(int[] array, int left, int right) {
	if(left >= right) {
		return;
	}
	// 抽出去的一个方法,确定了partition值,并将left->right元素以partition分隔,左边比partition值小,右边比partition大,最终返回partition所在index
	int partitionIndex = partition(array, left, right);
	quickSort(array, left, partitionIndex-1);
	quickSort(array, partitionIndex+1, right);
}

抽出的partition方法要完成从left->right-1的遍历,将比partition小的放前边,比partition大的放后边,伪代码如下:

private int partition(int[] array, int left, int right) {
	int partition = array[right];
	// 创建一个数组,存放left->right的上述三部分结果
	int[] temp = new int[right-left+1];
	int tempHead = 0;
	int tempTail = temp.length - 1;
	for(int i = left; i < right; i++) {
		if(array[i] < parition) {
			// 小的放前边
			temp[tempHead++] = array[i];
		} else {
			// 大的放后边
			temp[tempTail--] = array[i];
		}
	}
	// 循环结束后,一定会满足tempHead = tempTail的,而且这个位置还没有值,这里要放的就是分隔元素了
	temp[tempHead] = partition;
	// 将temp拷贝到array相应位置
	for(int i = 0; i < temp.length; i++{
		array[left+i] = temp[i];
	}
	// 最终返回partition在array中的索引,即left+tempHead
	return left+tempHead;
}

这样就结束了么,怎么看这里的partition和归并排序中的merge很类似了。这里有一种很骚的分区方式:不用创建临时数组temp,只靠比较加交换即可完成。

分区思路

一个数组从未分区到按尾节点分区完毕,变化类似于

[2,6,8,1,5,3]
[2,1,3,6,8,5]

这里不用关心左半部分和右半部分的顺序(我随意写的),主要关注分区完毕后,这个数组被分为三块,[2,1]是一块,3是一块,[6,8,5]是一块
只是通过比较加交换的话,就要在索引上做文章了。
其实在上边使用temp时不难发现,分隔元素的索引是最终确定下来的。
如果遍历顺序是从前到后,每有一个元素比分隔元素小,分隔元素的最终索引就要往后移动一位;如果遍历顺序是从后到前,每有一个元素比分隔元素大,分隔元素的最终索引就要往前移动一位。
初始化分隔元素的最终索引会是index=0,分隔元素3已经取出来了,从前到后和各元素比较,
2比,2更小,那么分隔元素的最终索引就变为了index=1,遍历数组的索引要往后移动一位了
6比,6更大,分隔元素的索引不用动(因为现在只知道2比分隔元素小,2占据了index=0,如果没有小于分隔元素的其他元素,那么分隔元素index=1就妥了),遍历数组的索引要往后移动一位了
8比,8更大,分隔元素的索引不用动,遍历数组的索引要往后移动一位了
1比,1更小,分隔元素的索引要后移一位变为index=2了,在此操作之前,要把元素1放到index=2之前,保证index=2之前的元素都比分隔元素小。这时候的操作就是将index=1位置的元素61交换。此时数组经历了第一次元素交换,变为了[2,1,8,6,5,3],遍历数组的索引要往后移动一位了
5比,5更大,分隔元素的索引不用动,遍历数组的索引要往后移动一位了
到了index=length-1要结束了。确定下来index=2就是分隔元素所在位置的索引,而目前占据该位置的元素肯定不比分隔元素小了,那么二者交换一下得到最终数组[2,1,3,6,5,8]
下边是伪代码

int partition = array[right];
int partitionIndex = left;
for(int i = left; i < right; i++) {
	if(array[i] < partition) {
		// 如果元素更小,分隔元素的索引后移一位,而且要把当前更小的元素和原来占据分隔元素索引位置的元素做交换,最终结果是新的分隔元素索引前的所有元素都比分隔元素要小
		int temp = array[i];
		array[i] = array[partitionIndex];
		array[partitionIndex] = temp;
		partitionIndex++;
	}
}
// 最后把分隔元素放到分隔元素索引处,也用到了交换
int temp = array[partitionIndex];
array[partitionIndex] = array[right];
array[right] = temp;

最终代码

import org.junit.Test;

import java.util.Arrays;

public class QuickSort {

    public void quickSort(int[] array, int left, int right) {
        if (left >= right) {
            return;
        }
        int partitionIndex = partition(array, left, right);
        quickSort(array, left, partitionIndex - 1);
        quickSort(array, partitionIndex + 1, right);
    }

    private int partition(int[] array, int left, int right) {
        int partition = array[right];
        int partitionIndex = left;
        for (int i = left; i < right; i++) {
            if (array[i] < partition) {
                int temp = array[i];
                array[i] = array[partitionIndex];
                array[partitionIndex] = temp;
                partitionIndex++;
            }
        }
        array[right] = array[partitionIndex];
        array[partitionIndex] = partition;
        return partitionIndex;
    }

    @Test
    public void test() {
        int[] array1 = {2, 6, 8, 1, 5, 3};
        System.out.println(Arrays.toString(array1));
        quickSort(array1, 0, array1.length - 1);
        System.out.println(Arrays.toString(array1));

        int[] array2 = {2, 6, 8, 1, 5, 3, 4};
        System.out.println(Arrays.toString(array2));
        quickSort(array2, 0, array2.length - 1);
        System.out.println(Arrays.toString(array2));
    }

}

你可能感兴趣的:(学习笔记)