至此,排序算法总结告一段落,然数据结构与算法知识庞大,围绕其展开的范围更是无限广大。
简单选择排序
思路:
简单选择排序和直接插入排序在我看来有些像,我们对比起来说。
直接插入排序在上上片文章(插入排序)中已经提过,它每次从未排序数组部分选第一个,然后拿到已排序数组部分一一比较,确定要插入的位置,插入。
而简单选择排序则是在未排序数组部分先选出当前最小的数,放到已排序数组部分的最后位置。
代码
public static void selectSort(int[] nums){
for(int i=0;i<nums.length;i++){
int point=i;
int min=nums[i];
for(int j=i;j<nums.length;j++){//每次从当前位置向后找最小的数据,将位置用point记录下来,最后交换最小数据point和当前位置i的值
if(min>nums[j]){
point=j;
min=nums[j];
}
}
swap(nums,i,point);
}
}
堆排序
堆排序使用最大堆这种结构,将选择最大元素这个操作的时间复杂度降到O(nlog2(n)的程度
这里就要先说一下堆这种数据结构了,堆是一种特殊的二叉树,这里以最大堆为例。规定子树的值必然小于本节点的值(最大堆),但不要求子树间满足大小关系,这样堆顶就是整个数最大的节点,整个堆是一个完全二叉树,可以用数组表示。每次取出堆顶节点后,通过从顶向下调整(ShiftDown),将下一次最大节点调整至堆顶。ShiftDown操作的复杂度是O(log(n))。
此外,堆还有一个建堆的操作,一组数据如何把她们构建成一棵最大堆呢,这里有一个叫做“筛选法”的方法(首先先把数组假想成一个完全二叉树,然后从最底层非叶子节点开始ShiftDown,直到最高层),建堆的复杂度是O(n)。
思路
堆排序借助堆,,将待排数组构建成一个基于数组的最大堆,每次取出堆顶最大元素,堆的规模减小,而将取出的数据插入到数组的最后。
这样,借助于堆的优良性能,堆排序的的复杂度是O(nlog2(n)。
关键点:
- 堆的建立:筛选法,关键是多次的ShiftDown
- 堆取出堆顶元素后的调整:对堆顶的ShiftDown
代码:
/**
*最大堆 向下调整的方法
*@param nums 待排数组,也是储存堆这种抽象数据结构的基础
*@param n 当前需要由上向下调整的根节点,为数组的下标
*@param length 当前堆的大小,因为待排数组和堆公用一个数组,有序的部分需要填充在数组后段,
如果不设置堆的边界length,那么每一次shiftDown整个数组都会再次成堆
*/
private void shiftDown(int[] nums,int n,int length){
int leftChild=n*2+1,rightChild=leftChild+1;//堆是完全二叉树,用数组储存,所以当前节点和左右子节点的下标有这样的对应关系
int root=n;
//有左右子节点,则比较左右子节点是否比当前节点大
if(leftChild<length && nums[root]<nums[leftChild]){
root=leftChild;
}
if(rightChild<length && nums[root]<nums[rightChild]){
root=rightChild;
}
//左右子节点比当前节点大,交换当前节点与较大的那个子节点的值
//对交换后的子节点递归调用shiftDown继续向下调整
if(root!=n){
int temp=nums[n];
nums[n]=nums[root];
nums[root]=temp;
shiftDown(nums,root,length);
}
}
/**
* 建最大堆的方法,使用了筛选法
*/
private void buildHeap(int[] nums){
int index=nums.length/2-1;//获得完全二叉树的最下一层最后一个有子节点的节点下标
for(int i=index;i>=0;i--){//由下向上ShiftDown构建最大堆
shiftDown(nums,i,nums.length);
}
}
public void heapSort(int[] nums){
buildHeap(nums);//首先,建队,这时整个数组就是一个最大堆
//循环取得最大堆的堆顶元素,插入到数组后段,同时缩小堆的大小
for(int i=nums.length-1;i>0;i--){
int max=nums[0];
nums[0]=nums[i];//将i位置的元素换上来,然后通过shiftDown调整最大堆
nums[i]=max;//大的元素交换到了数组后段,并且循环过程中完成了排序
shiftDown(nums,0,i);
}
}
归并排序
归并排序适合快速排序相当量级的排序算法,一般来说快速排序更适合于小数据集的排序,而归并排序因为其特点,在大量数据排序方面也是很好的。
思路
归并排序大方向上思想和快排一致,也是分治思想,关键就是如何分治了。
归并排序选择将待排数组分块,每一块分别使用归并排序,然后将块之间再次排序的方式排序。
这里的关键是如何在块之间再次排序,因为“块”最小的时候就是单个数据,无需排序。再次排序时,归并选择将两个块(在原数组中)复制一份,比较两个数组中的元素,将两个块中当前最小的元素放到原数组中,因为两个块都是有序的,所以在两个块中寻找最小的元素,就是比较遍历两个块的指针位置的数的大小(比较一次)。
关键点:
- 如何分治:这里又选择了递归的方式,递归的最底层是单个数的排序(本身有序,不用排),返回到上层就是两个数再次排序,然后是再上层……
代码:
public void mergeSort(int[] nums,int start,int end){
if(start<end){
int mid=(end+start)/2;
mergeSort(nums, start, mid);
mergeSort(nums, mid+1, end);//这里注意mid+1,没有的话可能引起无限的递归调用,进而栈溢出,eg:start=0 && end=1
merge(nums,start,mid,end);
}
}
private void merge(int[] nums,int start,int mid,int end){
int length1=mid-start+1;
int length2=end-mid;
int[] left=new int[length1];
int[] right=new int[length2];
//这里将原数组的两个块复制出来,重新排序
//相当于清空了原数组的这部分,从小到大往回塞
//使用Arrays.copyOfRang()是不是更简洁一点
for(int i=0;i<length1;i++){
left[i]=nums[start+i];
}
for(int i=0;i<length2;i++){
right[i]=nums[mid+i+1];
}
int i=0,j=0,k=start;
for(;k<end;k++){
if(i==length1||j==length2){
break;
}
if(left[i]<right[j]){
nums[k]=left[i++];
}else{
nums[k]=right[j++];
}
}
//如果复制出来的数组还没有全放进去,则全放回后面
while(i<length1){
nums[k++]=left[i++];
}
while(j<length2){
nums[k++]=right[j++];
}
}