排序
选择排序
选择排序开始的时候,我们扫描整个列表,找到它的最小元素,然后和第一个元素交换,将最小元素放到它在有序表中的最终位置上。然后我们从第二个元素开始扫描列表,找到最后n-1个元素中的最小元素,再和第二个元素交换位置,把第二小的元素放在它的最终位置上。
时间复杂度:n2,交换次数:n
冒泡排序
蛮力法在排序问题上还有另一个应用,它比较表中的相邻元素,如果它们是逆序的话就交换它们的位置。重复多次以后,最终,最大的元素就“沉到”列表的最后一个位置。第二遍操作将第二大的元素沉下去。
时间复杂度:n2,交换次数:n2
插入排序
一般来说,我们可以从右到左扫描这个有序的子数组,直到遇到第一个小于等于Am-1]的元素,然后把A[mー1]插在该元素的后面。这种算法被称为直接插入排序( straight insertion sort),或者简称为插入排序(insertion sort)。
时间复杂度:n2,交换次数:n
图4.4是该算法的操作图示。
89|45 68 90 29 34 17
45 89|68 90 29 34 17
图4.4用插入排序法进行排序的例子。一条竖线把输入的有序部分和剩下的元素分开正在插入的元素用粗体字表示
时间复杂度:n2,交换次数:n
归并排序
合并排序是成功应用分治技术的一个完美例子。对于一个需要排序的数组A[0.n-1]合并排序把它一分为二:A[0..(n/2)-1]和A[(n/2)..n-1],并对每个子数组递归排序,然后把这两个排好序的子数组合并为一个有序数组。
时间复杂度:nlogn
快速排序
快速排序是另一种基于分治技术的重要排序算法。不像合并排序是按照元素在数组中的位置对它们进行划分,快速排序按照元素的值对它们进行划分。我们在4.5节中讨论选择问题时介绍过数组划分的思想了。划分是对给定数组中的元素的重新排列,使得A[s]左边的元素都小于等于A[s],而所有A[s]右边的元素都大于等于A[s].
显然,建立了一个划分以后,A[j]已经位于它在有序数组中的最终位置,接下来我们可以继续对A[s]前和A[S]后的子数组分别进行排序(例如,使用同样的方法)。注意,它与合并排序的不同之处在于:在合并排序算法中,将问题划分成两个子问题是很快的,算法的主要工作在于合并子问题的解:而在快速排序中,算法的主要工作在于划分阶段,而不需要再去合并子问题的解了。
时间复杂度:nlogn,最差n2
堆排序
定义 堆(heap)可以定义为一棵二又树,树的节点中包含键(每个节点一个键),并且满足下面两个条件
(1)树的形状( shape property)要求一这棵二叉树是基本完备( essentially complete)的或者简称为完全二又树),这意味着,树的每一层都是满的,除了最后一层最右边的元素有可能缺位
(2)父母优势( parental dominance)要求,又称为堆特性( heap property)每一个节点的健都要大于或等于它子女的健(对于任何叶子我们认为这个条件都是自动满足的)
请注意,在堆中,键值是从上到下排序的。也就是说,在任何从根到某个叶子的路径上,鍵值的序列是递减的(如果允许相等的键存在,则是非递増的)。然而,键值之间并不存在从左到右的次序。也就是说,在树的同一层的节点之间,不存在任何关系,更一般地来说,在同一节点的左右子树之间也没有任何关系。
下面列出了堆的重要特性,这些特性都是不难证明的(作为一个例子,可以检查一下图6.10中堆的特性)
(1)只存在一棵个节点的完全二又树。它的高度等于[Log2n」。
(2)堆的根总是包含了堆的最大元素
(3)堆的一个节点以及该节点的子孙也是一个堆。
(4)可以用数组来实现堆,方法是用从上到下、从左到右的方式来记录堆的元素。为了方便起见,可以在这种数组从1到n的位置上存放堆元素,留下H[0],要么让它空着,要么在其中放一个限位器,它的值大于堆中的任何一个元素。在这种表示法中
a。父母节点的键将会位于数组的[n/2]个位置中,而叶子节点的键将会占据后[n/2]个位置
b。在数组中,对于一个位于父母位置i(1≤i[n/2])的键来说,它的子女将会位于2i和2i+1.相应地,对于一个位于i(2≤i≤n)的键来说,它的父母将会位于[i/2]
因此,我们也可以把堆定义为一个数组H[1..n],其中,数组前半部分中,每个位置i上的元素总是大于等于位置2i和2i+1中的元素,虽然对于大多数处理堆的算法来说把堆想象成二又树可以更容易地理解它们所隐含的思想,但对于实际实现来说,使用数组会简单得多,效率也高得多。
针对键的给定列表,我们如何来构造一个堆呢?我们有两种主要的做法。第一种是所谓的自底向上堆构造(ottm- up heap construction)算法(图6.11给出了图示)。在初始化一棵包含n个节点的完全二又树时,我们按照给定的顺序来放置键,然后按照下面的方法对树进行“堆化”。从最后的父母节点开始,到根为止,该算法检查这些节点的键是否满足父母优势要求。如果该节点不满足,该算法把节点的键K和它子女的最大键进行交换,然后再检查在新位置上,K是不是满足父母优势要求。这个过程一直继续到对K的父母优势要求满足为止(最终它必须满足,因为对于每个叶子中的键来说,这个条件是自动满足的)。
对于以当前父母节点为根的子树,在完成它的“堆化”以后,该算法对于该节点的直接前趋进行同样的操作。在对树的根完成这种操作以后,该算法就停止了。
现在我们可以描述堆排序( heapsort)了,威廉姆斯(J.W.J. Williams)发明了这一重要的排序算法(Wil64)。这种两阶段算法是这样工作的。
第一步:构造堆,即为一个给定的数组构造一个堆
第二步:删除最大键,即对剩下的堆应用n-1次根删除操作。
最终结果是按照降序刑除了该数组的元素。但是对于堆的数组实现来说,一个正在被删刚除的元素是位于最后的,所以结果数组将恰好是按照升序排列的原始数组。图6.14对个特定输入的堆排序进行了跟踪(我们故意使用了和图6.11中相同的输入,以便大家可以对这棵树和自底向上堆构造算法的数组实现进行比较)
时间复杂度:nlogn
希尔排序
所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间.
因此,我们需要采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
过大量的研究表明,当增量序列为 dlta[k]=2^(t-k+1)-1(0≤ k< [log(n+1)]时,可以获得不错的效率,其时间复杂度为O(n^(3/2)),要好于直接排序的O(n2)。需要注意的是,增量序列的最后一个增量值必须等于1オ行。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
排序代码如下:
package sort;
import java.util.ArrayList;
import java.util.Collections;
public class Sort implements Isort{
@Override
public void bubbleSort(int[] nums){
boolean hasChange = true;
for(int i = 0;i < nums.length - 1 && hasChange;i++){
hasChange = false;
for(int j = 0;j < nums.length - 1 - i;j++){
if(nums[j] > nums[j+1]){
hasChange = true;
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
@Override
public void insertionSort(int[] nums) {
for(int i = 1,j,current;i < nums.length;i++){
current = nums[i];
for(j = i - 1;j >= 0 && nums[j] > current;j--){
nums[j + 1] = nums[j];
}
nums[j + 1] = current;
}
}
@Override
public void mergeSort(int[] nums,int lo,int hi) {
if(lo >= hi){
return;
}
int mid = lo + (hi - lo) / 2;
mergeSort(nums,lo,mid);
mergeSort(nums,mid + 1,hi);
merge(nums,lo,mid,hi);
}
public void merge(int[] nums,int lo,int mid,int hi){
int[] copy = nums.clone();
int k = lo,i = lo,j = mid + 1;
while ((k <= hi)){
if(i > mid){
nums[k++] = copy[j++];
}else if(j > hi){
nums[k++] = copy[i++];
}else if(copy[i] > copy[j]){
nums[k++] = copy[j++];
}else{
nums[k++] = copy[i++];
}
}
}
@Override
public void quickSort(int[] nums,int lo,int hi) {
if(lo >= hi){
return;
}
int p = partition(nums,lo,hi);
quickSort(nums,lo,p - 1);
quickSort(nums,p + 1,hi);
}
public int partition(int[] nums,int lo,int hi){
int flag = lo + (int)(Math.random() * (hi - lo + 1));
swap(nums,flag,hi);
int i,j;
for(i = lo,j = lo;j < hi;j++){
if(nums[j] < nums[hi]){
swap(nums,i++,j);
}
}
swap(nums,i,j);
return i;
}
public void swap(int[] nums,int lo,int hi){
int temp = nums[hi];
nums[hi] = nums[lo];
nums[lo] = temp;
}
@Override
public void heaplSort(int[] nums) {
for(int i = nums.length / 2 - 1;i >= 0;i--){
preDown(nums,i,nums.length);
}
for(int i = nums.length - 1;i > 0;i--){
swap(nums,0,i);
preDown(nums,0,i);
}
}
public void preDown(int[] nums,int i,int n){
int child;
int tem;
for(tem = nums[i];leftChild(i) < n;i = child){
child = leftChild(i);
if(child != n-1 && nums[child] - nums[child + 1] < 0){
child++;
}
if(tem - nums[child] < 0){
nums[i] = nums[child];
}else{
break;
}
}
nums[i] = tem;
}
private int leftChild(int i){
return 2 * i + 1;
}
@Override
public void shellSort(int[] nums) {
int j;
for(int gap = nums.length / 2 ;gap > 0;gap /= 2)
for(int i = gap;i < nums.length;i++){
int temp = nums[i];
for(j = i;j >= gap && temp - nums[j - gap] < 0;j -= gap){
nums[j] = nums[j - gap];
}
nums[j] = temp;
}
}
@Override
public void bucketSort(int[] nums) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < nums.length; i++){
max = Math.max(max, nums[i]);
min = Math.min(min, nums[i]);
}
int bucketNum = (max - min) / nums.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
for(int i = 0; i < nums.length; i++){
int num = (nums[i] - min) / (nums.length);
bucketArr.get(num).add(nums[i]);
}
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
}