5477winter

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

十大排序算法

可视化:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
资料总结:https://www.bilibili.com/video/BV1DW4y1f7p8?spm_id_from=333.788.videopod.episodes&vd_source=69b667c94078fff1452cc5199e684fb7&p=51


比较排序

  • 冒泡
  • 插排
  • 选择排序
  • 希尔排序
  • 快速排序
  • 归并排序
  • 堆排序

非比较排序

  • 计数排序
  • 桶排序
  • 基数排序

1. 冒泡排序

算法过程:通过多次与相邻的元素比较与交换,使得最终的元素按升序排列。

可视化:
image

伪代码:

BubbleSort(arr):
    n = length(arr)
    for i = 0 to n-1:
        for j = 0 to n-i-2:
            if arr[j] > arr[j+1]:
                swap(arr[j], arr[j+1])

Java代码:

public class BubbleSort{
	public static void bubbleSort(int[] arr){
		int n = arr,length;
		for (int i = 0; i < n-1; i++){      //循环次数
			for (int j = 0 ;j < n-i-2){     //比较的两个元素
				if (arr[j] > arr[j+1]){
					int temp = arr[j];
					arr[j] = arr[j+1];
					arr[j+1] = temp;
				}
			}
		}
	}
}

时间复杂度:(稳定)

  • best:O(n)
  • worst:O(n²)
  • average:O(n²)
    // 外层循环n次,内层循环n-i次,总的执行次数为O(n²)

2. 插入排序

算法过程:后一位元素向前做升序(倒序)排序,若比上一个元素大(小)则不断往前排序,直到遇到比自己小(大)的元素;从第二个元素开始循环到最后一位元素。

可视化:

伪代码:

InsertSort(arr):
	n = length(arr);
	for(i = 1; i < n-1; i ++):  //循环次数为n次
		key = arr[i]
		j = i - 1
		while(j >= 0 && arr[j] > key): //内层循环:arr[i]=arr[j+1],前后元素比较大小,若前大于后则交换,再继续往前比较
			arr[j + 1] = arr[j]
			j = j - 1
		arr[j + 1] = key
	

Java代码:

public class InsertSort(){
	public static void insertSort(int[] arr){
		for(int i = 1; i < arr.length - 1; i ++){
			int key = arr[i];
			int j = i-1;
			while(j > 0 && arr[j] > key){
				arr[j + 1] = arr[j];
				j = j - 1;
			}
			arr[j + 1] = key;
		}
	}
}

时间复杂度:(稳定)

  • best:O(n)
  • worst:O(n²)
  • average:O(n²)
    // 外层循环n次,内层循环的执行次数与元素的插入位置有关,最差的情况为O(n)次,总的执行次数为O(n²)

3. 选择排序

算法过程:每次选择最小元素,将其放到已排序部分的末尾/开头 。

可视化:

伪代码:

SelectionSort(arr):
    n = length(arr)
    for i = 0 to n-1:  //外层循环次数
        min_index = i
        for j = i+1 to n:
            if arr[j] < arr[min_index]:
                min_index = j
        swap(arr[i], arr[min_index])

Java代码:

public class SelectionSort(){
	public static void selectionSort(int[] arr){
		int n = arr.length;
		for (int i = 0; i < n; i++){
			int min-index = 1;
			for(int j = i + 1; j < n; j++){
				if (arr[j] < arr[min-index]){
					min_index = j;
				}
			int temp = arr[i];
			arr[i] = arr[min_index];
			arr[min_index] = temp;
			}
		}
	}
}

时间复杂度:(稳定)

  • O(n²)
    // 选择排序中有两个嵌套循环,每次比较时都需要遍历整个未排序部分,总的执行次数为O(n²)

4.希尔排序

算法过程:将数组分割为若干个子数组,再分别对每个子数组进行插入排序,然后缩小子数组的间隔(例如第一次分割成数组里一定距离中每组只有两个的子数组,第二次为有四个元素的子数组...),最后对整体进行插入排序,直到整个数组有序。

可视化;image

伪代码:

ShellSort(arr):
	gap = length(arr) // 2
    while gap > 0:    //外循环: 
        for i = gap to length(arr)-1:
            temp = arr[i]
            j = i
            while j >= gap and arr[j - gap] > temp:   //如果当前元素arr[i]小于前面gap距离的元素arr[j-gap],则交换它们的位置。
                arr[j] = arr[j - gap]
                j = j - gap
            arr[j] = temp
        gap = gap // 2  //间隔继续减半,继续下一轮的排序

Java代码:

public class ShellSort{
	public static void shellSort(int[] arr){
		int n = arr.length;
		int gap = >> n;
		while(gap > 0){
			for(int i = gap; i < n-1; i++){
				temp = arr[i];
				int j = i;
				while(j >= gap && arr[j - gap] > temp){
					arr[j] = arr[j - gap];
					j = j - gap;
				}
				arr[j] = temp;
			}
			gap = >> gap;
		}
	}
}

时间复杂度:(不稳定)

  • best:O(nlogn)
  • worst:O(n²)
  • average:O(n^1.5)

5.快速排序

5.1 分治法

  1. 先从数组中取出一个数作为基准数
  2. 分区过程中,把大于这个数的数放在基准数的右边,把小于这个数的数放在基准数的左边。
  3. 在对左右区间重复第二步,直到各个区间只有一个数。

5.2 单向扫描分区方法

算法过程:设计数组第一个元素为基准数,数组左侧有一个左指针由数组第二个元素向右移动,数组右侧有一个右指针由数组最后一个元素向左移动,当左指针指到比基准数小的则左指针继续往右移动,若左指针指到比基准数大的元素则与右指针的元素比较,若右指针此时指的元素比基准数大,则右指针往左移动,直到找到比基准数小的元素,并和此时的左指针所指的元素交换,左指针继续往右移动。直到右指针在左指针左侧,右指针指向最后一个小于基准数的元素,并与基准数交换;(单向扫描)

然后到下一次递归,递归的数组区间在数组最左侧和基准数,在基准数最左侧的元素排序完后依照上述方法排序基准数右侧元素。(此处对应QuickSort伪代码)

伪代码:

QuickSort(arr, low, high):
    if low < high:
        pi = LomutoPartition(arr, low, high)    // 获取基准元素的正确位置
        QuickSort(arr, low, pi - 1)              // 对基准元素左侧的部分递归排序
        QuickSort(arr, pi + 1, high)             // 对基准元素右侧的部分递归排序
 
LomutoPartition(arr, low, high):
	pivot = arr[low]           // 选择基准元素为数组最左侧元素
	big = high                 // big是右指针,指向比基准元大的数
	sp = low + 1                   //sp为左指针/扫描指针
    while sp <= big:
		if arr[sp] <= pivot:
			sp ++
		else:
			swap arr[sp] and arr[big]
			big --
	swap arr[low] and arr[big]    //交换右指针元素和基准数
	return high                   //返回基准元的位置

Java代码:

public class QuickSortLomuto {
    // Lomuto单向分区方法
    public static int lomutoPartition(int[] arr, int low, int high) {
	int pivot = arr[low];
	int big = high;
	int sp = low + 1;
	while(sp <= big){
		if(arr[sp] <= pivot){
			sp ++;
		}else{
			int temp = arr[sp];
			arr[sp] = arr[big];
			arr[big] = temp;
			big --;
		}
	}
	int temp = arr[low];
	arr[low] = arr[big];
	arr[big] = temp;

	return big;
}

public static vid quickSort(int[] arr, int low, int high){
	if(low < high){
		pi = LomutoPartition(arr, low, high)    // 获取基准元素的正确位置
        QuickSort(arr, low, pi - 1)              // 对基准元素左侧的部分递归排序
        QuickSort(arr, pi + 1, high)             // 对基准元素右侧的部分递归排序
	}
}

时间复杂度:

  • best:O(nlogn)
  • worst:O(n²)
  • average:O(nlogn)

5.3 双向扫描分区方法

算法过程:设计数组的第一个元素为基准数,数组最左侧有一个左指针指向第二个元素,左指针指向的元素若小于等于基准数则继续向右移动,数组最右侧的右指针从最后一个元素开始向左移动,若指向大于基准数的元素则继续向左移动,若右指针指向的元素小于等于基准数,左指针指向的元素大于基准数,则左右两数交换;

当右指针在左指针左侧,则将基准数与右指针的元素交换。继续递归快速排序。

伪代码:

HoarePartition(arr, low, high):
	pivot = arr[low]           // 选择基准元素为数组最左侧元素
	right = high               // big是右指针,指向比基准元大的数
	left = low + 1             //sp为左指针,指向比基准元小的数
    while left <= right:
		while arr[left] <= pivot && left<= right:
			left ++
		while arr[right] > pivot && left <= right:
			right --
		swap arr[left] and arr[right] //当左指针元素大于基准数和右指针元素小于等于基准数时,左右相交换
	swap arr[low] and arr[right]   //交换右指针元素和基准数
	return right                   //返回基准元的位置

Java代码:

public class HoarePartition {
    //HoarePartition双向分区方法
    public static int HoarePartition(int[] arr, int low, int high) {
	int pivot = arr[low];
	int right = high;
	int left = low + 1;
	while(left <= right){
		while(arr[left] <= pivot && left <= right){
			left ++;
		}
		while(arr[right] > pivot && left <= right){
			right --;
		}
		int temp = arr[left];
		arr[left] = arr[right];
		arr[right] = temp;
	}
	int temp = arr[right];
	arr[right] = arr[low];
	arr[low] = temp;

	return big;
}

public static vid quickSort(int[] arr, int low, int high){
	if(low < high){
		pi = HoarePartition(arr, low, high)    // 获取基准元素的正确位置
        QuickSort(arr, low, pi - 1)              // 对基准元素左侧的部分递归排序
        QuickSort(arr, pi + 1, high)             // 对基准元素右侧的部分递归排序
	}
}

时间复杂度:

  • best:O(nlogn)
  • worst:O(n²)
  • average:O(nlogn)

5.4 有相同元素的快速排序

算法过程:有三个指针,一个指向比基准数小的元素(s),一个指向比基准数大的元素(b),一个指向与基准数相等的元素(e);s指针和e指针同时从第一个元素出发向右移动,当s,e指向的元素小于等于基准数的则继续向右移动,当等于时e指针指向等于的元素,若继续移动s直到遇到小于基准数的元素则s与e交换,并且s与e继续往左移动;当s指向的元素大于基准数时,s与b交换,b继续往右移动。

当s在b的右侧时停止排序,并且将基准数与arr[e-1]交换。

6.归并排序

算法过程:把数组递归分成两组,然后在各个两组内进行排序

伪代码:

MergeSort(arr):
    if length(arr) <= 1:
        return arr
    mid = length(arr) // 2
    left = MergeSort(arr[0..mid-1])
    right = MergeSort(arr[mid..end])
    return Merge(left, right)

Merge(left, right):
    result = []
    while left and right:
        if left[0] < right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
    result += left + right
	//在上述 while 循环结束后,left 和 right 中至少有一个数组为空。此时,另一个数组中可能还有元素(这些元素已经是有序的),将剩下的元素直接添加到 result 中。
    return result

Java代码:

public class MergeSort {
    public static int[] mergeSort(int[] arr) {
        if (arr.length <= 1) {
            return arr;
        }
        int mid = arr.length / 2;
        int[] left = mergeSort(Arrays.copyOfRange(arr, 0, mid));
        int[] right = mergeSort(Arrays.copyOfRange(arr, mid, arr.length));
        return merge(left, right);
    }

    private static int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        int i = 0, j = 0, k = 0;
        while (i < left.length && j < right.length) {
            if (left[i] < right[j]) {
                result[k++] = left[i++];
            } else {
                result[k++] = right[j++];
            }
        }
        while (i < left.length) {
            result[k++] = left[i++];
        }
        while (j < right.length) {
            result[k++] = right[j++];
        }
        return result;
    }
}

时间复杂度:O(nlogn)

7.堆排序

算法过程:从子树开始,若是小顶堆则将对比母节点和左右节点,若有小于的母节点的子节点则相互交换,然后递归到最终的根节点;大顶堆则相反。

伪代码:

【小顶堆-逆序】
MinHeap(arr):
	for x from n/2 - 1 to 0:
		n = arr.length
		MinHeapFixDown(arr,i,n);
MinHeapFixDown(arr,i,n):
	left = 2*i + 1   //左孩子节点位置
	right = 2*i + 2  //右孩子节点位置
	if left >= n :
		return       //判断左孩子越界
	min = left
	if right >= n:
		return       //判断右孩子越界
		min = left
	else:
		if arr[left]<arr[right]:
			min = right
	//判断左右节点哪个更小
	if arr[i] < arr[min]:
		return     //如果arr[i]比两个孩子节点都小则不用调整
	
	swap(arr[i],arr[min])
	//否则找到最小的子节点并交换
	MinHeapFixDown(arr,min,n)  //继续递归调整

Java代码:

【小顶堆-逆序】
public class HeapSort{
	public static void heapSort(int[] arr){
		for (int i = n/2 - 1; i >= 0;i --){
		int n = arr.length;
		MinHeapFixDown(arr,i,n);
	}
}
	
	public static void MinHeapFixDown(int[] arr,int i,int n){
		int left = 2*i + 1;   //左孩子节点位置
		int right = 2*i + 2;  //右孩子节点位置
		if(left >= n){
			return;       //判断左孩子越界
		}
		int min = left;
		if (right >= n){
			return;      //判断右孩子越界
			min = left;
		}
		else{
			if arr[left] < arr[right]{
				min = right;
			}
		}
		//判断左右节点哪个更小
		if (arr[i] < arr[min]){
			return;     //如果arr[i]比两个孩子节点都小则不用调整
		}
		int temp = arr[i];
		arr[i] = arr[min];
		arr[min] = temp;
		//否则找到最小的子节点并交换
		MinHeapFixDown(arr,min,n);  //继续递归调整
	}
}

时间复杂度:O(nlogn)

8.计数排序

算法过程:用一个辅助数组对数组中出现的元素进行计数,若出现n则在辅助数组的第n-1位会+1;

//优点:速度快
//缺点:数据范围大且系数,辅助空间大且系数,会造成空间的浪费

伪代码:

CountingSort(arr):
    k = max(arr)  // 找到数组中的最大值 k
    count = array of size (k + 1)  // 创建一个计数数组,记录每个元素的出现次数
    result = empty array of same size as arr  // 创建一个输出数组

    // 初始化计数数组
    for i from 0 to k:
        count[i] = 0  // 初始化每个元素出现的次数为 0

    // 统计每个元素出现的次数
    for i from 0 to length(arr) - 1:
        count[arr[i]] = count[arr[i]] + 1  // 对应元素的计数增加 1

    // 修改计数数组,使每个元素的值等于小于等于它的元素的总个数
    for i from 1 to k:
        count[i] = count[i] + count[i - 1]  // 将每个位置的值更新为该元素及前面元素的总数

    // 构造排序后的结果
    for i from length(arr) - 1 down to 0:
        result[count[arr[i]] - 1] = arr[i]  // 将元素放到输出数组中
        count[arr[i]] = count[arr[i]] - 1  // 更新计数数组中的次数

    return result  // 返回排序后的数组

Java代码:

import java.util.Arrays;

public class CountingSort {
    public static int[] countingSort(int[] arr) {
        // 如果数组为空或长度为1,直接返回原数组
        if (arr == null || arr.length <= 1) {
            return arr;
        }

        // 找到最大值和最小值
        int max = Arrays.stream(arr).max().getAsInt();
        int min = Arrays.stream(arr).min().getAsInt();

        // 计算计数数组的大小
        int range = max - min + 1;

        // 创建计数数组
        int[] count = new int[range];
        int[] result = new int[arr.length];

        // 统计每个元素的出现次数
        for (int i = 0; i < arr.length; i++) {
            count[arr[i] - min]++;  // 将元素值作为索引,增加计数
        }

        // 修改计数数组,累加每个位置的值
        for (int i = 1; i < range; i++) {
            count[i] += count[i - 1];  // 累加
        }

        // 构造排序后的数组
        for (int i = arr.length - 1; i >= 0; i--) {
            result[count[arr[i] - min] - 1] = arr[i];  // 根据计数将元素放到结果数组
            count[arr[i] - min]--;  // 更新计数
        }

        return result;  // 返回排序后的数组
    }

    public static void main(String[] args) {
        int[] arr = {4, 2, 2, 8, 3, 3, 1};
        System.out.println("Original Array: " + Arrays.toString(arr));
        int[] sortedArr = countingSort(arr);
        System.out.println("Sorted Array: " + Arrays.toString(sortedArr));
    }
}

时间复杂度::O(n+k)

9.桶排序

算法过程:先创建桶,将元素(value/(max=1)n)分配到各自的桶内后进行桶内的排序*,然后在将所有的桶内的元素按顺序合并在一起。

伪代码:

BucketSort(arr):
    n = length(arr)
    if n <= 1:
        return arr

    # Step 1: 找到数据的最小值和最大值
    min_value = min(arr)
    max_value = max(arr)

    # Step 2: 创建桶
    num_buckets = n
    bucket_range = (max_value - min_value) / num_buckets
    buckets = new List[num_buckets]
    
    # Step 3: 将元素分配到桶中
    for i = 0 to n-1:
        bucket_index = (arr[i] - min_value) / bucket_range
        buckets[bucket_index].add(arr[i])
    
    # Step 4: 对每个桶内的元素进行排序
    for i = 0 to num_buckets-1:
        sort(buckets[i])  # 可以使用插入排序或其他排序
    
    # Step 5: 合并桶内元素
    result = []
    for each bucket in buckets:
        result.addAll(bucket)
    
    return result

Java代码;

import java.util.*;

public class BucketSort {
    public static void bucketSort(float[] arr) {
        int n = arr.length;
        if (n <= 1) {
            return;
        }

        // Step 1: 找到数据的最小值和最大值
        float minValue = arr[0];
        float maxValue = arr[0];
        for (int i = 1; i < n; i++) {
            if (arr[i] < minValue) minValue = arr[i];
            if (arr[i] > maxValue) maxValue = arr[i];
        }

        // Step 2: 创建桶
        int numBuckets = n;
        List<Float>[] buckets = new List[numBuckets];
        for (int i = 0; i < numBuckets; i++) {
            buckets[i] = new ArrayList<>();
        }

        // Step 3: 将元素分配到桶中
        float range = (maxValue - minValue) / numBuckets;
        for (int i = 0; i < n; i++) {
            int index = (int) ((arr[i] - minValue) / range);
            if (index == numBuckets) {
                index--;
            }
            buckets[index].add(arr[i]);
        }

        // Step 4: 对每个桶内的元素进行排序
        for (int i = 0; i < numBuckets; i++) {
            Collections.sort(buckets[i]);
        }

        // Step 5: 合并桶内元素
        int index = 0;
        for (int i = 0; i < numBuckets; i++) {
            for (float num : buckets[i]) {
                arr[index++] = num;
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        float[] arr = {0.42f, 0.32f, 0.23f, 0.11f, 0.56f, 0.78f};
        bucketSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

时间复杂度:

  • best:O(nlogn)
  • worst:O(n²)

10.基数排序

算法过程:通过按位对数字进行排序,逐步从最低位到最高位进行排序。其排序过程是分步进行的,每步依赖于数字的某一位(从最低位开始),使用稳定的排序算法(如计数排序)对当前位进行排序。

伪代码:

RadixSort(arr):
    max = findMax(arr)  // 找到数组中的最大值
    exp = 1  // exp表示当前位,初始为1(即最低位)
    while max / exp > 0:  // 当最高位的数字还未被处理时,循环
        countingSortByDigit(arr, exp)
        exp *= 10  // 处理下一个位

countingSortByDigit(arr, exp):
    n = length(arr)
    output = new array of size n
    count = new array of size 10 (for digits 0 to 9)

    // 计算每个数字在当前位的出现次数
    for i = 0 to n-1:
        index = (arr[i] / exp) % 10
        count[index] += 1

    // 修改count数组,count[i]保存的是小于等于i的元素数量
    for i = 1 to 9:
        count[i] += count[i-1]

    // 构建输出数组
    for i = n-1 down to 0:
        index = (arr[i] / exp) % 10
        output[count[index] - 1] = arr[i]
        count[index] -= 1

    // 将输出数组复制到原数组
    for i = 0 to n-1:
        arr[i] = output[i]

Java代码:

import java.util.Arrays;

public class RadixSort {
    
    public static void radixSort(int[] arr) {
        // 找到最大值
        int max = findMax(arr);
        
        // 从最低位开始排序
        for (int exp = 1; max / exp > 0; exp *= 10) {
            countingSortByDigit(arr, exp);
        }
    }
    
    // 找到数组中的最大值
    private static int findMax(int[] arr) {
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        return max;
    }

    // 根据当前位exp进行计数排序
    private static void countingSortByDigit(int[] arr, int exp) {
        int n = arr.length;
        int[] output = new int[n];
        int[] count = new int[10];  // 计数数组,存储每个数字出现的频率

        // 计算当前位的数字的频率
        for (int i = 0; i < n; i++) {
            int index = (arr[i] / exp) % 10;
            count[index]++;
        }

        // 修改计数数组,count[i]表示小于等于i的数字个数
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }

        // 从后往前遍历原数组,确保排序的稳定性
        for (int i = n - 1; i >= 0; i--) {
            int index = (arr[i] / exp) % 10;
            output[count[index] - 1] = arr[i];
            count[index]--;
        }

        // 将排序后的数组复制回原数组
        System.arraycopy(output, 0, arr, 0, n);
    }

    // 测试基数排序
    public static void main(String[] args) {
        int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
        System.out.println("Original Array: " + Arrays.toString(arr));
        radixSort(arr);
        System.out.println("Sorted Array: " + Arrays.toString(arr));
    }
}

时间复杂度:O(d * (n + k))

十种排序算法的时间复杂度总结

image

posted on 2025-01-27 03:56  5477  阅读(50)  评论(0)    收藏  举报