十大排序算法(带注释)

术语

  • 时间复杂度:算法执行所消耗的时间。
  • 空间复杂度:算法执行所消耗的存储空间。
  • 稳定排序:相同的两个元素,排序前后顺序不变,用于两个排序关键字的情况,如对价格升序的同时销量也升序。
  • 不稳定排序:相同的两个元素,排序前后顺序可能发生改变,如快速排序、希尔排序、选择排序、堆排序(快些选堆)。
  • 原地排序:无需额外存储空间,直接在原数组上进行排序,所以会修改原数组。
  • 非原地排序:需要利用额外的存储空间来辅助排序。

一、选择排序:选择最小元素与首元素交换

1、找到数组中最小的那个元素,将它和数组的第一个元素交换位置;

2、这样一趟比较下来,排在第一的元素就会是最小的数;

2、在剩下的元素中找到最小的元素,将它和数组的第二个元素交换位置;

3、如此往复,直到将整个数组排序。

/**
 * <dl>
 * <dt><b>1.选择排序</b></dt>
 * <dd>每一轮选出最小值,交换到左侧</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void selectSort(int[] a) {
	int n = a.length;
	int minIndex = 0;

	// 只需比较n-1轮
	for (int i = 0; i < n - 1; i++) {
		minIndex = i;
		// 每轮比较n-1-i次
		for (int j = i + 1; j < n; j++) {
			if (a[j] < a[i]) {
				// 最小值下标
				minIndex = j;
			}
		}

		// 最小值与a[i]交换
		int temp = a[i];
		a[i] = a[minIndex];
		a[minIndex] = temp;
	}
}

性质:

  1. 时间复杂度:O(n2)
  2. 空间复杂度:O(1)
  3. 不稳定排序
  4. 原地排序

二、插入排序:将元素插入到不比它大的位置(插扑克牌)

1、从数组第2个元素开始抽取元素;

2、把它与左边第一个元素比较,如果左边第一个元素比它大,则继续与左边第二个元素比较下去,直到遇到不比它大的元素,然后插到这个元素的右边;

3、这样一趟比较下来,该元素已经被插入到适当的位置,其左侧都不比它大;

4、继续选取第3,4,….n个元素,重复步骤 2 ,选择适当的位置插入。

/**
 * <dl>
 * <dt><b>2.插入排序</b></dt>
 * <dd>维护一个有序区,把元素一个一个插入到有序区的适当位置,直到所有元素有序为止</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void insertSort(int[] a) {
    int n = a.length;
    int j = 0;
    int insValue = 0;

    // 只需比较n-1轮
    for (int i = 0; i < n - 1; i++) {
        // 假定a[0]有序,从a[1]开始
        j = i + 1;
        // 待插入值
        insValue = a[j];

        // 比待插入值大的元素(待插入值前面的)后移,给待插入值腾出位置
        while (j > 0 && insValue < a[j - 1]) {
            a[j] = a[j - 1];
            j -= 1;
        }

        // 把待插入值放在适当位置上
        a[j] = insValue;
    }
}

性质:

  1. 时间复杂度:O(n2)
  2. 空间复杂度:O(1)
  3. 稳定排序
  4. 原地排序

三、冒泡排序:元素两两比较,最大元素交换到末尾

1、把第一个元素与第二个元素比较,如果第一个比第二个大,则交换他们的位置;

2、接着继续比较第二个与第三个元素,如果第二个比第三个大,则交换他们的位置;

3、这样一趟比较下来,排在最后的元素就会是最大的数;

4、如此往复,直到将整个数组排序。

/**
 * <dl>
 * <dt><b>3.冒泡排序(标准版)</b></dt>
 * <dd>两两比较,左比右大就交换,最大的数冒泡到右侧</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void bubbleSort(int[] a) {
    int n = a.length;
    // 交换用临时变量
    int temp = 0;

    // 只需比较n-1轮
    for (int i = 0; i < n - 1; i++) {
        // 每轮比较n-1-i次
        for (int j = 1; j < n; j++) {
            if (a[j - 1] > a[j]) {
                temp = a[j - 1];
                a[j - 1] = a[j];
                a[j] = temp;
            }
        }
    }
}

性质:

  1. 时间复杂度:O(n2)
  2. 空间复杂度:O(1)
  3. 不稳定排序
  4. 原地排序

假如从开始的第一对到结尾的最后一对,相邻的元素之间都没有发生交换的操作,这意味着右边的元素总是大于等于左边的元素,此时的数组已经是有序的了,我们无需再对剩余的元素重复比较下去了。

优化之后的代码如下所示:

/**
 * <dl>
 * <dt><b>3.冒泡排序(优化版)</b></dt>
 * <dd>两两比较,左比右大就交换,最大的数冒泡到右侧</dd>
 * <dd>1.如果一轮比较后没有交换,那么数组已经有序</dd>
 * <dd>2.每排序一轮,内层循环的次数可以减1</dd>
 * <dd>3.用异或交换值</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void bubbleSortOptimization(int[] a) {
    // 优化2:内层循环次数
    int loops = a.length;
    // 优化1:交换标志
    boolean swapFlag = false;

    // 只需比较n-1轮
    for (int i = 0; i < a.length - 1; i++) {
        // 每轮循环都要初始化交换标志
        swapFlag = false;

        // 每轮比较loops - 1次
        for (int j = 1; j < loops; j++) {
            if (a[j - 1] > a[j]) {
                // 优化3:用异或交换值
                a[j - 1] = a[j - 1] ^ a[j];
                a[j] = a[j] ^ a[j - 1];
                a[j - 1] = a[j - 1] ^ a[j];

                // 已交换
                swapFlag = true;

            }
        }

        // 缩减内层循环次数
        loops--;

        // 本轮没有交换,结束
        if (!swapFlag) {
            return;
        }
    }
}

四、希尔排序:将插入排序的1替换成逐步折半的step

希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。

1、让数组中任意间隔为 step = n / 2 的元素有序;

2、让数组中任意间隔为 step = n / 4 的元素有序;

3、直到数组中任意间隔为 step = 1 的元素有序,此时整个数组就是有序的。

/**
 * <dl>
 * <dt><b>4.希尔排序</b></dt>
 * <dd>将插入排序的1替换成逐步折半的step</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void shellSort(int[] a) {
    int n = a.length;
    int j = 0;
    int insValue = 0;

    // 采用逐步折半的增量方法
    for (int step = n / 2; step > 0; step /= 2) {
        // 只需比较n-1轮
        for (int i = 0; i < n - step; i++) {
            // 假定a[0]有序,从a[step]开始
            j = i + step;
            // 待插入值
            insValue = a[j];

            // 比待插入值小的元素(待插入值前面的)后移,给待插入值腾出位置
            while (j > 0 && insValue < a[j - step]) {
                a[j] = a[j - step];
                j -= step;
            }

            // 把待插入值放在适当位置上
            a[j] = insValue;
        }
    }
}

性质:

  1. 时间复杂度:O(nlogn)
  2. 空间复杂度:O(1)
  3. 不稳定排序
  4. 原地排序

五、归并排序:将左右数组排序,然后归并

1、通过递归的方式将大的数组一直分割,直到数组的大小为 1,此时只有一个元素,那么该数组就是有序的了;

2、把两个数组大小为1的合并成一个大小为2的数组;

3、把两个数组大小为2的合并成一个大小为4的数组;

4、直到全部小的数组合并起来,此时整个数组就是有序的。

/** 辅助数组 */
private static int[] aux = null;

/**
 * <dl>
 * <dt><b>5.归并排序</b></dt>
 * <dd>将左右数组排序,然后归并</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void mergeSort(int[] a) {
    // 辅助数组分配内存空间
    aux = new int[a.length];

    // 自顶向下
//		upToDown(a, 0, a.length - 1);
    // 自底向上
    downToUp(a);
}

/**
 * <dl>
 * <dt><b>5.归并排序(自顶向下)</b></dt>
 * <dd>递归调用</dd>
 * </dl>
 * @param a 待排序数组
 * @param left 要排序的最小下标
 * @param right 要排序的最大下标
 */
public static void upToDown(int[] a, int left, int right) {
    // 左右指针相遇时返回
    if (left >= right) {
        return;
    }

    int mid = left + (right - left) / 2;

    // 将左边排序
    upToDown(a, left, mid);
    // 将右边排序
    upToDown(a, mid + 1, right);
    // 归并
    merge(a, left, mid, right);
}

/**
 * <dl>
 * <dt><b>5.归并排序(自底向上)</b></dt>
 * <dd>先两两归并,再四四归并,以此类推</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void downToUp(int[] a) {
    int n = a.length;

    // a[0...sz...sz+sz-1]排序,a[sz+sz...sz+sz+sz-1...sz+sz+sz+sz-1]排序
    // 每次都是sz+sz个元素排序
    for (int sz = 1; sz < n; sz = sz + sz) {
        for (int left = 0; left < n - sz; left += sz + sz) {
            merge(a, left, left + sz - 1, Math.min(left + sz + sz - 1, n - 1));
        }
    }
}

/**
 * <dl>
 * <dt><b>5.归并排序(归并)</b></dt>
 * <dd>先将数组复制到辅助数组,然后左右数组分别归并到原数组</dd>
 * </dl>
 * @param a 待排序数组
 * @param left 要排序的最小下标
 * @param mid 中间下标
 * @param right 要排序的最大下标
 */
public static void merge(int[] a, int left, int mid, int right) {
    for (int i = left; i <= right; i++) {
        // 辅助数组初始化
        aux[i] = a[i];
    }

    int i = left;
    int j = mid + 1;
    // 归并回原数组
    for (int k = left; k <= right; k++) {
        if (i > mid) {
            // 左边用尽,取右边元素
            a[k] = aux[j++];
        } else if (j > right) {
            // 右边用尽,取左边元素
            a[k] = aux[i++];
        } else if (aux[i] > aux[j]) {
            // 取最小的,即右边元素
            a[k] = aux[j++];
        } else {
            // 取最小的,即左边元素
            a[k] = aux[i++];
        }
    }
}

性质:

  1. 时间复杂度:O(nlogn)
  2. 空间复杂度:O(n)
  3. 稳定排序
  4. 非原地排序

六、快速排序:选择一个切分元素使得左边的元素都小于它,右边的元素都不小于它

1、选择一个切分元素,暂存到tmp变量;

2、交换左边大于tmp和右边小于tmp的元素;

3、将切分元素与左右相遇位置元素交换;

4、如此往复,直到将整个数组排序。

/**
 * <dl>
 * <dt><b>6.快速排序</b></dt>
 * <dd>选择一个切分元素,暂存到tmp变量中</dd>
 * <dd>使得左边的元素都比他小,右边的都比它大</dd>
 * <dd>如此循环,直到左右指针相遇</dd>
 * </dl>
 * @param a 待排序数组
 * @param left 待排序的最小下标
 * @param right 待排序的最大下标
 */
public static void quickSort(int[] a, int left, int right) {
    // 左右指针相遇时返回
    if (left >= right) {
        return;
    }

    // 切分元素的下标
    int mid = partition(a, left, right);
    // 继续对左侧切分
    quickSort(a, left, mid - 1);
    // 继续对右侧切分
    quickSort(a, mid + 1, right);

}

/**
 * @param a 待排序数组
 * @param left 待排序的最小下标
 * @param right 待排序的最大下标
 * @return 切分元素下标
 */
public static int partition(int[] a, int left, int right) {
    // 暂存切分元素
    int pivot = a[left];
    int i = left;
    int j = right + 1;
    int tmp = 0;

    while (true) {
        // 找到左侧大于切分元素的下标
        while (a[++i] <= pivot) {
            // 直到最大下标也没找到
            if (i == right) {
                break;
            }
        }

        // 找到右侧小于切分元素的下标
        while (a[--j] >= pivot) {
            // 直到最小下标也没找到
            if (j == left) {
                break;
            }
        }

        // 左右指针相遇,结束
        if (i >= j) {
            break;
        }

        // 交换a[i]和a[j]
        tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }

    // 交换a[left]和a[j]
    a[left] = a[j];
    // 使中轴元素处于有序的位置
    a[j] = pivot;

    return j;
}

性质:

  1. 时间复杂度:O(nlogn)
  2. 空间复杂度:O(logn)
  3. 不稳定排序
  4. 原地排序

七、堆排序:将无序数组构建成排序二叉堆

1、把无序数组构建成二叉堆;

2、堆顶堆底元素互换;

3、调节打乱后的堆,产生新的堆顶。

/**
 * <dl>
 * <dt><b>7.堆排序</b></dt>
 * <dd>把无序数组构建成二叉堆</dd>
 * <dd>循环删除堆顶元素,移到集合尾部,调节打乱后的堆,产生新的堆顶</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void heapSort(int[] a) {
    int n = a.length;

    // 把无序数组构建成二叉堆
    for (int i = (n - 2) / 2; i >= 0; i--) {
        downAdjust(a, i, n);
    }

    int tmp = 0;
    // 排序二叉堆
    for (int i = n - 1; i > 0; i--) {
        // 堆顶堆底元素互换
        tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;

        // 调节打乱后的堆,产生新的堆顶
        downAdjust(a, 0, i);
    }
}

/**
 * <dl>
 * <dt><b>下沉调整</b></dt>
 * </dl>
 * @param a 待调整的堆
 * @param parent 待下沉的父节点
 * @param length 堆的有效大小
 */
public static void downAdjust(int[] a, int parent, int length) {
    // 保存父节点的值
    int tmp = a[parent];
    // 左子节点
    int child = parent * 2 + 1;

    while (child < length) {
        if (child + 1 < length && a[child + 1] > a[child]) {
            // 指向最大的那个子节点
            child++;
        }

        // 如果父节点大于等于最大子节点的值,无需下沉,跳出
        if (tmp >= a[child]) {
            break;
        }

        // 子节点上浮
        a[parent] = a[child];
        // 当前子节点变为父节点
        parent = child;
        // 当前子节点的子节点
        child = child * 2 + 1;
    }

    // 父节点移动到合适的位置
    a[parent] = tmp;
}

性质:

  1. 时间复杂度:O(nlogn)
  2. 空间复杂度:O(1)
  3. 不稳定排序
  4. 原地排序

八、计数排序:统计字母个数,创建长度27的数组,每个元素表示字母出现的次数

计数排序是一种适合于最大值和最小值的差值不是不是很大的排序。

1、假设数组取值范围[0, max],那么创建一个长度为max+1的计数数组;

2、数值出现一次,计数数组对应元素值+1,然后将计数数组写回原数组。

/**
 * <dl>
 * <dt><b>8.计数排序(标准版)</b></dt>
 * <dd>假设数组取值范围[0, max],那么创建一个长度为max+1的计数数组</dd>
 * <dd>数值出现一次,计数数组对应元素值+1,然后将计数数组写回原数组</dd>
 * @param a 待排序数组
 * </dl>
 */
public static void countSort(int[] a) {
    int max = a[0];

    for (int i = 1; i < a.length; i++) {
        // 找出最大值
        if (a[i] > max) {
            max = a[i];
        }
    }

    // 计数数组
    int[] counts = new int[max + 1];
    for (int i : a) {
        // 计数
        counts[i]++;
    }

    int index = 0;
    for (int i = 0; i < counts.length; i++) {
        while (counts[i] > 0) {
            // 回写
            a[index++] = i;
            // 计数减1
            counts[i]--;
        }
    }
}

性质:

  1. 时间复杂度:O(n + k)
  2. 空间复杂度:O(k)
  3. 稳定排序
  4. 非原地排序

注:k 表示临时数组的大小

上面的代码中,我们是根据 max 的大小来创建对应大小的数组,假如原数组只有10个元素,并且最小值为 min = 10000,最大值为 max = 10005,那我们创建 10005 + 1 大小的数组不是很吃亏,最大值与最小值的差值为 5,所以我们创建大小为6的临时数组就可以了。

也就是说,我们创建的临时数组大小 (max - min + 1)就可以了,然后在把 min作为偏移量。优化之后的代码如下所示:

/**
 * <dl>
 * <dt><b>8.计数排序(优化版)</b></dt>
 * <dd>计数数组的大小设置为max-min+1</dd>
 * @param a 待排序数组
 * </dl>
 */
public static void countSortOptimization(int[] a) {
    int max = a[0];
    int min = a[0];

    for (int i = 1; i < a.length; i++) {
        // 找出最值
        max = Math.max(a[i], max);
        min = Math.min(a[i], min);
    }

    // 计数数组
    int[] counts = new int[max - min + 1];

    for (int i : a) {
        // 计数
        counts[i - min]++;
    }

    int index = 0;
    for (int i = 0; i < counts.length; i++) {
        while (counts[i] > 0) {
            // 回写
            a[index++] = i + min;
            // 计数减1
            counts[i]--;
        }
    }
}

九、桶排序:以十位上的数字划分0~9共10个桶,对应数字分别放入桶中进行排序,然后再进行归并

1、创建(max-min)/length+1个桶,相应地将i元素放入(a[i]-min)/length桶中;

2、再对每个桶进行排序,可以使用快速排序、归并排序等;

3、将排序后的桶依次写回数组,此时整个数组就是有序的。

/**
 * <dl>
 * <dt><b>9.桶排序</b></dt>
 * <dd>创建(max-min)/length+1个桶,相应地将i元素放入(a[i]-min)/length桶中</dd>
 * <dd>再对每个桶进行排序,最后将排序后的桶依次写回数组</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void bucketSort(int[] a) {
    int min = Integer.MAX_VALUE;
    int max = Integer.MIN_VALUE;

    for (int i : a) {
        min = Math.min(min, i);
        max = Math.max(max, i);
    }

    int n = a.length;
    int bucketLength = (max - min) / n + 1;

    // 用ArrayList作为桶,每个桶里面还是一个ArrayList
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketLength);
    for (int i = 0; i < bucketLength; i++) {
        bucketArr.add(new ArrayList<Integer>());
    }

    int bucketIndex = 0;
    for (int i = 0; i < n; i++) {
        // 对应桶下标
        bucketIndex = (a[i] - min) / n;
        // 放入对应桶
        bucketArr.get(bucketIndex).add(a[i]);
    }

    int index = 0;
    // 可以使用各种方法对每个桶排序,并放入数组
    for (ArrayList<Integer> arrayList : bucketArr) {
        Collections.sort(arrayList);
        for (int i : arrayList) {
            a[index] = i;
            index++;
        }
    }
}

性质:

  1. 时间复杂度:O(n + k)
  2. 空间复杂度:O(n + k)
  3. 稳定排序
  4. 非原地排序

注:k 表示桶的个数

十、基数排序:从个位开始分别放入桶中,然后写回数组,个位、十位依次有序

1、个位放入对应的桶,再将桶中数据写回数组,此时个位有序;

2、十位放入对应的桶,再将桶中数据写回数组,此时十位有序;

3、如此往复,直到将整个数组排序。

/**
 * <dl>
 * <dt><b>10.基数排序</b></dt>
 * <dd>个位放入对应的桶,清空桶,十位放入对应的桶,清空桶</dd>
 * </dl>
 * @param a 待排序数组
 */
public static void radixSort(int[] a) {
    // 10进制
    int radix = 10;
    int n = a.length;

    int max = Integer.MIN_VALUE;
    for (int i : a) {
        // 最大值
        max = Math.max(max, i);
    }

    for (int i = 1; max / i > 0; i *= radix) {

        int[][] buckets = new int[radix][n];
        int remainder = 0;
        for (int j = 0; j < n; j++) {
            // 求当前位
            remainder = (a[j] / i) % radix;
            // 放入对应的桶中
            buckets[remainder][j] = a[j];
        }

        // 桶中数据放入数组,第一轮个位有序,第二轮十位有序
        int k = 0;
        for (int[] bucket : buckets) {
            for (int buc : bucket) {
                if (buc != 0) {
                    a[k] = buc;
                    k++;
                }
            }
        }
    }
}

性质:

  1. 时间复杂度:O(n * k)
  2. 空间复杂度:O(n + k)
  3. 稳定排序
  4. 非原地排序

注:k 表示桶的个数

算法总结

排序算法平均时间复杂度最好最坏空间复杂度稳定性
选择排序 O(n^2) O(n^2) O(n^2) O(1) 不稳定
插入排序 O(n^2) O(n) O(n^2) O(1) 稳定
冒泡排序 O(n^2) O(n) O(n^2) O(1) 稳定
希尔排序 O(n^1.3) O(n) O(nlog2n) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
快速排序 O(nlogn) O(nlogn) O(n^2) O(logn) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
计数排序 O(n+k) O(n+k) O(n+k) O(k) 稳定
桶排序 O(n+k) O(n+k) O(n+k) O(n+k) 稳定
基数排序 O(n*k) O(n*k) O(n*k) O(n+k) 稳定
posted @ 2021-02-13 16:57  ageovb  阅读(233)  评论(0编辑  收藏  举报