几种排序算法基本原理及Java实现


排序算法有很多种,其基本原理都是将数组中逆序的部分进行交换,得到最终顺序排列的数组。如 [ 3 2 1 8 5 9 ] 中,若要将其从小到大排列,则逆序排列对有 [ 3 2 ],[ 3 1 ],[ 2 1 ],[ 8 5 ] 等等。最后我们需要得到的是一个完全顺序排列的数组,也就是 [ 1 2 3 5 8 9 ],可以看到其中没有逆序排列对,排序算法也就此完成。

选择排序

从一个数组中选择最小的元素,将它与数组的第一个元素交换位置,再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置,依此类推,直到整个数组排序完成。

如 [ 3 2 1 8 5 9 ] 中,将最小元素 1 与第一个元素 3 互换位置,将第二小的元素 2 与第二个元素互换位置(此时不变),将第三小的元素 3 与第三个元素互换位置(此时已交换过,位置不变),依此类推,最后得到 [ 1 2 3 5 8 9 ]。

public void sort(Double nums[]) {
    for(int i = 0; i < nums.length - 1; i++) {
        int min = i;
        for(int j = i + 1; j < nums.length; j++) {
            if(less(nums[j], nums[min])) {
                min = j;
            }
        }
        swap(nums, i, min);
    }
}

冒泡排序

从头开始遍历数组,若当前元素大于其右边的元素,则将其位置交换,直到数组的尾部,完成一轮循环。第二次循环仍然从数组的第一个元素开始,但是到数组的倒数第二个元素终止。后续循环依此类推。若一轮循环没有发生位置交换,则程序终止,完成将数组从小到大的排序。

如 [ 3 2 1 8 5 9 ] 中,比较第一个元素和第二个元素的大小,互换位置得到 [ 2 3 1 8 5 9 ],继续比较得到 [ 2 1 3 8 5 9 ] -> [ 2 1 3 8 5 9 ] -> [ 2 1 3 5 8 9 ] -> [ 2 1 3 5 8 9 ],第一轮循环结束。后续循环依此类推,最终可以得到 [ 1 2 3 5 8 9 ]。

public void sort(Double nums[]){
    boolean isSorted = false;
    for(int i = 0; i < nums.length - 1 && !isSorted; i++) {
        isSorted = true;
        for(int j = 0; j < nums.length - i - 1; j++) {
            if(less(nums[j+1], nums[j])) {
                isSorted = false;
                swap(nums, j, j+1);
            }
        }
    }
}

插入排序

将数组分为左右两个子数组进行判断,其中左边是排序好的数组部分,右边是待排序的数组部分。

  1. 取数组的第一个元素当作左边数组,将数组从第二个元素开始的所有元素当作右边数组。如 [ 3 2 1 8 5 9 ],将其分为 [ 3 ] 和 [ 2 1 8 5 9 ] 两个部分。
  2. 从右边数组的第一个元素开始,用冒泡排序的思想将其与左边数组进行比较排序,若当前元素小于前一个元素,则将其位置互换,直到判断到左边数组的第一个元素。如将 2 和 3 比较,2 < 3,将其位置互换,得到的结果为 [ 2 3 1 8 5 9 ]。
  3. 重复上述步骤,此时将 1 与 3 比较,1 < 3,互换后得到 [ 2 1 3 8 5 9 ],继续将1与2比较,1 < 2,互换后得到 [ 1 2 3 8 5 9 ]。
  4. 继续重复上述步骤,最后可以得到排序完的数组为 [ 1 2 3 5 8 9 ]。
public void sort(Double nums[]) {
    for(int i = 1; i < nums.length; i++) {
        for(int j = i; j > 0 && less(nums[j], nums[j-1]); j--){
            swap(nums, j, j-1);
        }
    }
}

希尔排序

在面对大规模的数组时,插入排序/冒泡排序等的效率会显得比较低,因为一次只能交换数组中相邻两个元素的位置,也就是减少了一个逆序对。如果有一种方法可以一次减少多个逆序对,那么效率也会提高很多。希尔排序就是这样一种排序方法,通过交换不相邻的元素,一次可以减少一到多个逆序对。

将数组按一定的增量h进行分组排序,h通常设置为1,4,13,40,121等等。如对于一个有100个元素的数组,可以将h设置为40,即将序号为0,40,80的三个元素编为一组进行插入排序,将序号为1,41,81的元素编为一组进行插入排序…第一轮排序完之后继续以13为增量进行排序,依此类推,直到最后以1为增量将整个数组排序完成。

希尔排序的优势主要在于将多个间隔较远的元素进行排序,跳过了中间项,因此一次排序可能会减少多个逆序对,如 [ 8 2 1 3 5 4 6 ] 中,将 8 和 3 互换,一次可以减少 [ 8 2 ],[ 8 1 ],[ 8 3 ]三个逆序对,从而提升排序的效率。当h减小之后,由于此时数组中的元素经过了几轮排序之后,都比较靠近它们排序后的位置,因为采用插入排序可以更快地达到目标。

在代码实现中,我们不需要额外再加一个循环将数组进行分组,而是可以从第h个元素开始,直接对该元素所处的组进行处理。如 [ 8 2 1 3 5 4 6 ] 中,设 h = 3,循环会从元素 3 开始,第一步将其与间隔 3 的前一个元素 8 进行比较,再将 5 与 2 进行比较,4 与 1进行比较,6 与 3 和 8 进行比较,完成第一次循环。第二次循环可以设 h = 1,依此类推,最后得到排序完成的数组。

public void sort(Double nums[]) {
    int N = nums.length;
    int h = 1;
    while(h < N / 3) {
        h = h * 3 + 1;
    }
    while(h >= 1) {
        for(int i = h; i < N; i++) {
            for(int j = i; j >= h && less(nums[j], nums[j-h]); j -= h) {
                swap(nums, j, j - h);
            }
        }
        h = (h - 1) / 3;
    }
}

归并排序

  • 将一个需要排序的数组从中间隔开,如 [ 2 0 1 4 7 6 8 2 ],分为两个部分分别进行排序,即 [ 2 0 1 4 ] [ 7 6 8 2 ],得到两个排完序的数组 [ 0 1 2 4 ] [ 2 6 8 7 ]。
  • 将这两个数组归并到一起进行排序,比较两个数组当前元素的大小,将较小的那一个元素添加到新的数组中,在例子中即为将 0 添加进新数组中
  • 重复上操作,直到其中一个数组被添加完毕,如 [ 0 1 2 2 4 ] 时,将另一个数组剩下的所有元素添加进新数组中,完成排序,得到 [ 0 1 2 2 4 6 7 8 ]。
public void merge(T[] nums, int l, int m, int h, T backup[]) { // l是要排序的数组起点,h是终点,m是分割点
    int i = l, j = m + 1;
    for (int k = l; k <= h; k++) {
        backup[k] = nums[k]; // 将数组备份
    }
    for (int k = l; k <= h; k++) {
        if (j > h) {
            nums[k] = backup[i++]; // 如果第一部分添加完毕,则将第二部分的元素依次加入数组中
        } else if (i > m) {
            nums[k] = backup[j++]; // 如果第二部分添加完毕,则将第一部分的元素依次加入数组中
        } else if (backup[i].compareTo(backup[j]) <= 0) {
            nums[k] = backup[i++]; // 比较大小,取小的添加进数组
        } else {
            nums[k] = backup[j++]; // 比较大小,取小的添加进数组
        }
    }
}

在前述将数组分成两部分分别排序的步骤中,我们可以通过自顶向下的思想,即每次都将数组分为2个子数组进行处理,再将2个子数组分为4个子数组…直到不可再分,即将 [ 2 0 1 4 7 6 8 2 ] 分为 [ 2 0 ],[ 1 4 ],[ 7 6 ],[ 8 2 ],对每个数组分别进行排序,再将其两两结合进行排序,重复该步骤,最后组成排序完的数组。

public void sort(Double nums[]) {
    Double backup[] = new Double[nums.length];
    upDownSort(nums, 0, nums.length - 1, backup);
}

public void upDownSort(Double nums[], int l, int h, Double backup[]) {
    if (l < h) {
        int mid = (l + h) / 2;
        upDownSort(nums, l, mid, backup);
        upDownSort(nums, mid + 1, h, backup);
        merge(nums, l, mid, h, backup);
    }
}

快速排序

快速排序的基本思想是通过数组中的一个切分元素将数组切分为左右两个子数组,左边数组的所有元素小于等于切分元素,右边数组的所有元素大于切分元素,只要将这两个子数组分别排序并结合起来就可以得到最终排序完成的数组。如 [ 2 0 1 4 7 6 8 2 ] 中,取第一个元素2为切分元素,则左边数组为 [ 0 1 2 ],右边数组为 [ 4 7 6 8 ],将这两个子数组分别排序并结合即可得到最终结果。步骤如下:

  • 将第一个元素作为切分元素,设置两个指针分别指向数组的第二个元素和最后一个元素;
  • 若左指针指向的元素小于等于切分元素,则指针继续指向后一个元素,直到左指针指向的元素大于切分元素或达到数组最后一个元素时停止;若右指针指向的元素大于切分元素,则指针继续指向前一个元素,直到右指针指向的元素小于等于切分元素或达到数组第二个元素时停止;
  • 当左右指针停止移动后交换两个指针对应的元素,即左指针指向的一个大于切分元素的元素和右指针指向的一个小于等于切分元素的元素互换,并继续两个指针的相向移动;
  • 当两个指针相遇之后再将切分元素和右指针位置互换,即可得到以切分元素为界的两个数组,继续重复上述操作即可得到最终排序的数组。
private void sort(Double[] nums, int l, int h) {
    if (l < h) {
        int mid = getIndex(nums, l, h);
        sort(nums, l, mid - 1);
        sort(nums, mid + 1, h);
    }
}

private int getIndex(Double[] nums, int l, int h){
    Double backup = nums[l];
    int leftPointer = l + 1, rightPointer = h;
    while(true) {
        while(less(nums[leftPointer], backup) && leftPointer < h) {
            leftPointer++;
        }
        while(less(backup, nums[rightPointer]) && rightPointer > l) {
            rightPointer--;
        }
        if(leftPointer >= rightPointer) {
            break;
        }
        swap(nums, leftPointer, rightPointer);
    }
    swap(nums, l, rightPointer);
    return rightPointer;
}

对于有大量重复元素的数组,可以使用三向切分, 即将数组分为大于、小于和等于三个部分进行排序。

  • 建立三个指针left、mid、right,初始位置分别指向数组头部、头部+1、数组尾部。
  • 比较当前mid指向的元素与首位元素的大小,若大于则将mid与right的元素互换,使大于首位元素的都排在右边,此时right指向的元素来到mid的位置,再将right-1,代表一个大于首位元素的元素已经被添加至右边部分;若小于则将mid与left的元素互换,使小于的元素都排在左边,此时首位元素来到mid的位置,再将left和mid同时+1,代表一个小于首位元素的元素已经被添加至左边部分,并继续寻找下一个元素;若等于则仅将mid+1,继续寻找下一个元素。
  • 重复以上步骤,直到mid与right相遇,此时数组已分为小于、等于、大于首位元素的三个部分,仅将小于和大于的部分继续排序即可。
private void sort(Double[] nums, int l, int h) {
    if (l < h) {
        int leftPointer = l, rightPointer = h, midPointer = l + 1;
        Double backup = nums[l];
        while(midPointer <= rightPointer) {
            if(less(nums[midPointer], backup)) {
                swap(nums, leftPointer++, midPointer++);
            } else if(less(backup, nums[midPointer])) {
                swap(nums, midPointer, rightPointer--);
            } else {
                midPointer++;
            }
        }
        sort(nums, l, leftPointer - 1);
        sort(nums, rightPointer + 1, h);
    }
}

堆排序

完全二叉树的性质:将完全二叉树自顶向下,同一层自左向右编号,根节点的编号为0。一个节点k若存在两个叶子节点,则这两个叶子节点对应的编号为2k+1和2k+2,其父节点的编号为(k-1)/2向下取整。
排序流程:

  1. 将一个数组从头到尾按照自左向右、自顶向下的顺序构成一个完全二叉树,并自右向左对有叶子节点的节点搜寻
  2. 判断该父节点与其一到二个子节点的值大小,并将最大值与父节点的值交换(若有必要)保证父节点的元素最大
  3. 继续取该父节点的前一个节点(由完全二叉树的性质,也是父节点),重复步骤2,直到对根节点完成排序
  4. 将根节点(最大值)与最后一个节点的元素交换位置,对前N-1个元素组成的完全二叉树重复上述操作,直到全部完成排序
public void sort(Double[] nums) {
    int N = nums.length; // 最后一个元素的编号为N-1
    for(int i = N / 2 - 1; i >= 0; i--) {
        sink(nums, i, N); // 自右向左对有叶子节点的节点搜寻
    }

    while(N > 1) {
        swap(nums, 0, --N); // 将根节点和最后一个节点的值互换
        sink(nums, 0, N); // 重新对忽略掉最后一个节点(最大值)的数组进行堆排序
    }
}

public void sink(Double[] nums, int i, int N) {
    while(i < N / 2) {
        int j = 2 * i + 1; // 取当前节点的第一个子节点
        if(j < N - 1 && less(nums[j], nums[j+1])) {
            j++; // 若右节点的值较大则将指针指向改节点
        }
        if(!less(nums[i], nums[j])) {
            break; // 当父节点的值大于子节点时直接跳出
        }
        swap(nums, i, j); // 当父节点的值小于子节点的时候交换
        i = j; // 继续向下面的子节点搜寻
    }
}

几种排序方法计算速度的比较代码

通过随机生成一个长度为20000的数组,并用这几种方法分别进行排序,可以得到下面的结果。
在这里插入图片描述

public static void main(String args[]) {
    Choose choose = new Choose();
    Bubble bubble = new Bubble();
    Insert insert = new Insert();
    Shell shell = new Shell();
    Random rand = new Random();
    UpDownMerge upDown = new UpDownMerge();
    QuickSort quickSort = new QuickSort();
    HeapSort heapSort = new HeapSort();
	int length = 20000;
	Double[] a = new Double[length];
	Double[] b = new Double[length];
	Double[] c = new Double[length];
	Double[] d = new Double[length];
	Double[] e = new Double[length];
	Double[] f = new Double[length];
	Double[] g = new Double[length];
	for (int i = 0; i < length; i++) {
		a[i] = rand.nextDouble();
		b[i] = a[i];
        c[i] = a[i];
        d[i] = a[i];
        e[i] = a[i];
        f[i] = a[i];
        g[i] = a[i];
    }
    double start = System.currentTimeMillis();
    choose.sort(a);
    double stop1 = System.currentTimeMillis();
    bubble.sort(b);
    double stop2 = System.currentTimeMillis();
    insert.sort(c);
    double stop3 = System.currentTimeMillis();
    shell.sort(d);
    double stop4 = System.currentTimeMillis();
    upDown.sort(e);
    double stop5 = System.currentTimeMillis();
    quickSort.sort(f);
    double stop6 = System.currentTimeMillis();
    heapSort.sort(f);
    double stop7 = System.currentTimeMillis();

    System.out.println("选择排序用时为" + (stop1 - start));
    System.out.println("冒泡排序用时为" + (stop2 - stop1));
    System.out.println("插入排序用时为" + (stop3 - stop2));
    System.out.println("希尔排序用时为" + (stop4 - stop3));
    System.out.println("自顶向下归并排序用时为" + (stop5 - stop4));
    System.out.println("快速排序用时为" + (stop6 - stop5));
    System.out.println("堆排序用时为" + (stop7 - stop6));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值