데이터 구조 및 알고리즘 3가지 정렬

1. 단순 정렬

우리 프로그램에서 정렬은 매우 일반적인 요구 사항입니다.일부 데이터 요소가 제공되며 이러한 데이터 요소는 특정 규칙에 따라 정렬됩니다. 예를 들어 일부 주문을 쿼리하고 주문 날짜별로 정렬하고, 또 다른 예로는 일부 상품을 쿼리하고 상품 가격에 따라 정렬하는 등의 작업을 수행할 수 있습니다. 그래서, 다음으로 우리는 몇 가지 일반적인 정렬 알고리즘을 배워야 합니다.

자바 개발 툴킷 jdk에서는 API 형태로 제공되는 List, Set, Map, Math 등 많은 데이터 구조와 알고리즘 구현을 제공했으며, 이 방법의 장점은 한 번 쓸 수 있다는 것 많은 곳에서 사용됩니다. 우리는 jdk 메소드에서 학습하여 알고리즘을 특정 클래스로 캡슐화하는데, 이 경우 자바 코드를 작성하기 전에 먼저 API를 설계하고 설계가 완료된 후에 이러한 API를 구현해야 합니다.

예를 들어 먼저 다음과 같이 API 세트를 설계합니다.
여기에 이미지 설명 삽입

1.1 Comparable 인터페이스 소개

여기서는 정렬에 대해 이야기할 것이기 때문에 반드시 요소를 비교할 것이고 Java는 정렬 규칙을 정의하기 위해 Comparable 인터페이스를 제공합니다.

필요:

  1. 연령 및 이름 사용자 이름의 두 가지 속성으로 학생 클래스 학생을 정의하고 Comparable 인터페이스를 통해 비교 규칙을 제공합니다.
  2. 테스트 클래스 Test를 정의하고 테스트 클래스 Test에 테스트 메서드 Comparable getMax(Comparable c1, Comparable c2)를 정의하여 테스트를 완료합니다.
//学生类
public class Student implements Comparable<Student>{
    
    
	private String username;
	private int age;
	public String getUsername() {
    
    
		return username;
	}
	public void setUsername(String username) {
    
    
		this.username = username;
	}
	public int getAge() {
    
    
		return age;
	}
	public void setAge(int age) {
    
    
		this.age = age;
	}
	
	@Override
	public String toString() {
    
    
		return "Student{" +
		"username='" + username + '\'' +
		", age=" + age +
		'}';
	}
		
	//定义比较规则
	@Override
	public int compareTo(Student o) {
    
    
		return this.getAge()-o.getAge();
	}
}

//测试类
public class Test {
    
    
	public static void main(String[] args) {
    
    
		Student stu1 = new Student();
		stu1.setUsername("zhangsan");
		stu1.setAge(17);
		Student stu2 = new Student();
		stu2.setUsername("lisi");
		stu2.setAge(19);
		Comparable max = getMax(stu1, stu2);
		System.out.println(max);
	}
	//测试方法,获取两个元素中的较大值
	public static Comparable getMax(Comparable c1,Comparable c2){
    
    
		int cmp = c1.compareTo(c2);
		if (cmp>=0){
    
    
			return c1;
		}else{
    
    
			return c2;
		}
	}
}

1.2 버블 정렬

버블 정렬은 컴퓨터 과학 분야에서 비교적 간단한 정렬 알고리즘입니다.

요구 사항:
정렬 전: {4,5,6,3,2,1}

정렬 후: {1,2,3,4,5,6}

분류 원리:

  1. 인접한 요소를 비교합니다. 이전 요소가 후자보다 크면 두 요소의 위치를 ​​바꿉니다.
  2. 시작 부분의 첫 번째 쌍에서 끝 부분의 마지막 쌍까지 각 인접 요소 쌍에 대해 동일한 작업을 수행합니다. 마지막으로 마지막 위치의 요소가 최대값입니다.

여기에 이미지 설명 삽입
버블 정렬 API 설계:
여기에 이미지 설명 삽입
버블 정렬의 코드 구현:

//排序代码
public class Bubble {
    
    
	/*
	对数组a中的元素进行排序
	*/
	public static void sort(Comparable[] a){
    
    
		for(int i=a.length-1;i>0;i--){
    
    
			for (int j = 0; j <i; j++) {
    
    
				if (greater(a[j],a[j+1])){
    
    
					exch(a,j,j+1);
				}
			}
		}
	}
	
	/*
	比较v元素是否大于w元素
	*/
	private static boolean greater(Comparable v,Comparable w){
    
    
		return v.compareTo(w)>0;
	}
	/*
	数组元素i和j交换位置
	*/
	private static void exch(Comparable[] a,int i,int j){
    
    
		Comparable t = a[i];
		a[i]=a[j];
		a[j]=t;
	}
}


//测试代码
public class Test {
    
    
	public static void main(String[] args) {
    
    
		Integer[] a = {
    
    4, 5, 6, 3, 2, 1};
		Bubble.sort(a);
		System.out.println(Arrays.toString(a));
	}
}

버블 정렬의 시간 복잡도 분석

버블정렬은 이중층 for 루프를 사용하며, 내부 루프의 루프 몸체가 실제로 정렬을 완료하는 코드이기 때문에 버블 정렬의 시간복잡도를 분석하는데, 주로 내부 루프 몸체의 실행 시간을 분석한다.

최악의 경우, 즉 정렬할 요소가 {6,5,4,3,2,1}의 역순인 경우
요소 비교 수는 다음과 같습니다.

 (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

요소 교환의 수는 다음과 같습니다.

 (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

총 실행 수는 다음과 같습니다.

 (N^2/2-N/2)+(N^2/2-N/2)=N^2-N;

Big O 파생 규칙에 따르면 함수의 최상위 항목이 유지되므로 최종 버블 정렬의 시간 복잡도는 O(N^2)입니다.

1.3 선택 정렬

선택 정렬은 더 간단하고 직관적인 정렬 방법입니다.

요구 사항:
정렬 전: {4,6,8,7,9,2,10,1}
정렬 후: {1,2,4,5,7,8,9,10}

분류 원리:

  1. 각 순회시 첫 번째 인덱스에 있는 요소를 최소값으로 가정하고 이를 다른 인덱스에 있는 값과 차례로 비교하여 현재 인덱스에 있는 값이 다른 인덱스에 있는 값보다 큰 경우 , 다른 인덱스 값이 최소값이라고 가정하고 최종적으로 최소값이 위치한 인덱스를 찾을 수 있습니다.
  2. 가장 작은 값이 위치한 인덱스와 첫 번째 인덱스의 값을 교환

여기에 이미지 설명 삽입

선택 정렬 API 설계:
여기에 이미지 설명 삽입

선택 정렬의 코드 구현:

//排序代码
public class Selection {
    
    
	/*
	对数组a中的元素进行排序
	*/
	public static void sort(Comparable[] a){
    
    
		for (int i=0;i<=a.length-2;i++){
    
    
			//假定本次遍历,最小值所在的索引是i
			int minIndex=i;
			for (int j=i+1;j<a.length;j++){
    
    
				if (greater(a[minIndex],a[j])){
    
    
				//跟换最小值所在的索引
				minIndex=j;
				}
			}
			//交换i索引处和minIndex索引处的值
			exch(a,i,minIndex);
		}
	}
	
	/*
	比较v元素是否大于w元素
	*/
	private static boolean greater(Comparable v,Comparable w){
    
    
		return v.compareTo(w)>0;
	}
	
	/*
	数组元素i和j交换位置
	*/
	private static void exch(Comparable[] a,int i,int j){
    
    
		Comparable t = a[i];
		a[i]=a[j];
		a[j]=t;
	}
}

//测试代码
public class Test {
    
    
	public static void main(String[] args) {
    
    
		Integer[] a = {
    
    4,6,8,7,9,2,10,1};
		Selection.sort(a);
		System.out.println(Arrays.toString(a));
	}
}

선택 정렬의 시간 복잡도 분석:
선택 정렬은 외부 루프가 데이터 교환을 완료하고 내부 루프가 데이터 비교를 완료하는 이중 계층 for 루프를 사용하므로 각각 데이터 교환 및 데이터 비교 수를 계산합니다.

데이터 비교 횟수:

 (N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

데이터 교환 횟수:
N-1

시간 복잡도:N^2/2-N/2+(N-1)=N^2/2+N/2-1;

Big O 파생 규칙에 따르면 최상위 항목이 유지되고 상수 요소가 제거되며 시간 복잡도는 O(N^2)입니다.

1.4 삽입 정렬

삽입 정렬은 간단하고 직관적이며 안정적인 정렬 알고리즘입니다.
삽입 정렬은 사람들이 포커 패를 정렬하는 것과 매우 유사하게 작동합니다. 시작하려면 왼손이 비어 있고 테이블의 카드가 아래를 향하도록 합니다. 그런 다음 테이블에서 한 번에 한 장의 카드를 가져와 왼손의 올바른 위치에 삽입합니다. 카드의 올바른 위치를 찾기 위해 아래 그림과 같이 이미 손에 있는 각 카드를 오른쪽에서 왼쪽으로 비교합니다.
여기에 이미지 설명 삽입
요구 사항:
정렬 전: {4,3,2,10,12,1, 5 ,6}
정렬 후: {1,2,3,4,5,6,10,12}

분류 원리:

  1. 모든 요소를 ​​정렬된 그룹과 정렬되지 않은 두 그룹으로 나눕니다.
  2. 정렬되지 않은 그룹에서 첫 번째 요소를 찾아 정렬된 그룹에 삽입합니다.
  3. 정렬된 요소를 역순으로 탐색하여 삽입할 요소와 차례로 비교하여 삽입할 요소보다 작거나 같은 요소를 찾은 후 이 위치에 삽입할 요소를 놓고 이동 다른 요소는 1비트 뒤로;

여기에 이미지 설명 삽입
삽입 정렬 API 설계:
여기에 이미지 설명 삽입
삽입 정렬 코드 구현:

public class Insertion {
    
    
	/*
	对数组a中的元素进行排序
	*/
	public static void sort(Comparable[] a){
    
    
		for (int i=1;i<a.length;i++){
    
    
			//当前元素为a[i],依次和i前面的元素比较,找到一个小于等于a[i]的元素
			for (int j=i;j>0;j--){
    
    
				if (greater(a[j-1],a[j])){
    
    
					//交换元素
					exch(a,j-1,j);
				}else {
    
    
					//找到了该元素,结束
					break;
				}
			}
		}
	}
	
	/*
	比较v元素是否大于w元素
	*/
	private static boolean greater(Comparable v,Comparable w){
    
    
		return v.compareTo(w)>0;
	}
	/*
	数组元素i和j交换位置
	*/
	private static void exch(Comparable[] a,int i,int j){
    
    
		Comparable t = a[i];
		a[i]=a[j];
		a[j]=t;
	}
}

삽입정렬의 시간 복잡도 분석

삽입정렬은 이중층 for 루프를 사용하며, 내부 루프의 루프 몸체가 실제로 정렬을 완료하는 코드이기 때문에 삽입 정렬의 시간복잡도를 분석하며 주로 내부 루프 몸체의 실행시간을 분석한다.

최악의 경우, 즉 정렬할 배열 요소가 {12,10,6,5,4,3,2,1}이면
비교 횟수는 다음과 같습니다.

(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

교환 횟수는 다음과 같습니다.

(N-1)+(N-2)+(N-3)+...+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

총 실행 수는 다음과 같습니다.

(N^2/2-N/2)+(N^2/2-N/2)=N^2-N;

Big O 파생 규칙에 따르면 함수의 최상위 항목이 유지되므로 최종 삽입 정렬의 시간 복잡도는 O(N^2)입니다.

2. 고급 정렬

버블정렬, 선택정렬, 삽입정렬 등 기본적인 정렬은 앞에서 배웠고 최악의 경우 시간복잡도를 분석한 결과 모두 O(N^2)이고 제곱순은 통과함 알고리즘 분석을 배우기 전에 , 우리는 입력 규모가 증가함에 따라 시간 비용이 급격히 증가하므로 이러한 기본적인 정렬 방법은 더 큰 규모의 문제를 처리할 수 없다는 것을 알고 있습니다.다음으로 알고리즘의 시간 복잡도를 줄이기 위해 몇 가지 고급 정렬 알고리즘을 배웁니다. 최고 권력에.

2.1 힐 정렬

힐 정렬은 삽입 정렬 알고리즘의 보다 효율적이고 개선된 버전인 "축소 증분 정렬"이라고도 하는 삽입 정렬 유형입니다.

삽입정렬에 대해 알아보다 보면 매우 불친절한 점을 발견하는데, 정렬된 그룹핑 요소가 {2,5,7,9,10}이고 정렬되지 않은 그룹핑 요소가 {1,8}이면 다음으로 삽입할 요소는 is 1. 실제 삽입을 완료하려면 10, 9, 7, 5, 2와 위치를 교환해야 합니다. 각 교환은 인접 요소 위치와만 교환할 수 있습니다. 그럼 효율성을 높이고자 한다면 직관적인 생각은 하나의 교환이 1을 더 높은 위치에 놓을 수 있다는 것입니다. 이러한 요구 사항을 실현하는 방법은 무엇입니까? 다음으로 Hill Sorting의 원리를 살펴보자.

요구 사항:
정렬 전: {9,1,2,5,7,4,8,6,3,5}
정렬 후: {1,2,3,4,5,5,6,7,8,9}

분류 원리:

  1. 증가량 h를 선택하고 증가량 h에 따라 데이터를 그룹핑의 기준으로 그룹화하는 단계;
  2. 그룹으로 나누어진 각 데이터 그룹에 대한 완전한 삽입 정렬;
  3. 증가량을 줄이고 최소값은 1이며 두 번째 단계를 반복합니다.

여기에 이미지 설명 삽입

성장량 h의 결정: 성장량 h의 가치에 대한 고정된 규칙은 없습니다.여기서 우리는 다음 규칙을 채택합니다.

int h=1
while(h<5){
    
    
	h=2h+1//3,7
}
//循环结束后我们就可以确定h的最大值;
h的减小规则为:
	h=h/2

여기에 이미지 설명 삽입
힐 정렬의 코드 구현:

//排序代码
public class Shell {
    
    
	/*
	对数组a中的元素进行排序
	*/
	public static void sort(Comparable[] a){
    
    
		int N = a.length;
		//确定增长量h的最大值
		int h=1;
		while(h<N/2){
    
    
			h=h*2+1;
		}
		//当增长量h小于1,排序结束
		while(h>=1){
    
    
			//找到待插入的元素
			for (int i=h;i<N;i++){
    
    
				//a[i]就是待插入的元素
				//把a[i]插入到a[i-h],a[i-2h],a[i-3h]...序列中
				for (int j=i;j>=h;j-=h){
    
    
					//a[j]就是待插入元素,依次和a[j-h],a[j-2h],a[j-3h]进行比较,如果a[j]小,那么
					交换位置,如果不小于,a[j]大,则插入完成。
					if (greater(a[j-h],a[j])){
    
    
						exch(a,j,j-h);
					}else{
    
    
						break;
					}
				}
			}
			h/=2;
		}
	}
	
	/*
	比较v元素是否大于w元素
	*/
	private static boolean greater(Comparable v,Comparable w){
    
    
		return v.compareTo(w)>0;
	}
	
	/*
	数组元素i和j交换位置
	*/
	private static void exch(Comparable[] a,int i,int j){
    
    
		Comparable t = a[i];
		a[i]=a[j];
		a[j]=t;
	}
}


//测试代码
public class Test {
    
    
	public static void main(String[] args) {
    
    
		Integer[] a = {
    
    9,1,2,5,7,4,8,6,3,5} ;
		Shell.sort(a);
		System.out.println(Arrays.toString(a));
	}
}

Hill Sorting의 시간 복잡도 분석 Hill Sorting은
증가량 h에 대한 고정된 규칙이 없으며, 많은 논문에서 다양한 증가 시퀀스를 연구했지만 어느 것도 특정 시퀀스가 ​​최고라는 것을 증명할 수 없습니다. For Hill의 시간 복잡도 분석은 정렬은 코스 설계 범위를 벗어나므로 여기서는 분석하지 않습니다.

사후 분석을 사용하여 Hill 정렬과 삽입 정렬의 성능을 비교할 수 있습니다.
역방향 데이터를 100000에서 1로 저장하는 새 reverse_shell_insertion.txt 파일을 생성하면 이 데이터 배치를 기반으로 테스트를 완료할 수 있습니다. 테스트 아이디어: 정렬을 수행하기 전 시간을 기록하고 정렬이 완료된 후 시간을 기록합니다. 두 시간의 시간 차이가 정렬 시간을 소모합니다.

힐 정렬 및 삽입 정렬 성능 비교 테스트 코드:

public class SortCompare {
    
    
	public static void main(String[] args) throws Exception{
    
    
		ArrayList<Integer> list = new ArrayList<>();
		//读取reverse_arr.txt文件
		BufferedReader reader = new BufferedReader(new InputStreamReader(new
		FileInputStream("reverse_shell_insertion.txt")));
		String line=null;
		while((line=reader.readLine())!=null){
    
    
			//把每一个数字存入到集合中
			list.add(Integer.valueOf(line));
		}
		reader.close();
		//把集合转换成数组
		Integer[] arr = new Integer[list.size()];
		list.toArray(arr);
		testInsertion(arr);//使用插入排序耗时:20859
		// testShell(arr);//使用希尔排序耗时:31
	}
	
	public static void testInsertion(Integer[] arr){
    
    
		//使用插入排序完成测试
		long start = System.currentTimeMillis();
		Insertion.sort(arr);
		long end= System.currentTimeMillis();
		System.out.println("使用插入排序耗时:"+(end-start));
	}
	
	public static void testShell(Integer[] arr){
    
    
		//使用希尔排序完成测试
		long start = System.currentTimeMillis();
		Shell.sort(arr);
		long end = System.currentTimeMillis();
		System.out.println("使用希尔排序耗时:"+(end-start));
	}
}

테스트를 통해 대량의 데이터 배치를 처리할 때 Hill 정렬의 성능이 삽입 정렬보다 실제로 더 높다는 것을 알 수 있습니다.

2.2 병합 정렬

2.2.1 재귀

재귀 정렬을 정식으로 배우기 전에 재귀 알고리즘에 대해 배워야 합니다.
정의:
메서드를 정의할 때 메서드 내에서 메서드 자체를 호출하는 것을 재귀라고 합니다.

public void show(){
    
    
	System.out.println("aaaa");
	show();
}

기능:
일반적으로 크고 복잡한 문제를 레이어별로 원래 문제와 유사한 소규모 문제로 변환하여 해결합니다. 재귀 전략은 문제 해결 프로세스에 필요한 여러 번의 반복 계산을 설명하기 위해 적은 수의 프로그램만 필요하므로 프로그램의 코드 양이 크게 줄어듭니다.

참고:
재귀에서는 자신을 무제한으로 호출할 수 없습니다. 재귀 호출이 스택 메모리의 새 공간을 열고 메서드를 다시 실행하기 때문에 재귀가 종료될 수 있도록 경계 조건이 있어야 합니다. 재귀 수준이 너무 높으면 스택 메모리 오버플로가 발생하기 쉽습니다.

여기에 이미지 설명 삽입
요구 사항:
N의 계승을 완료하기 위해 재귀를 사용하는 방법을 정의하십시오.

分析:
1!: 1
2!: 2*1=2*1!
3!: 3*2*1=3*2!
4!: 4*3*2*1=4*3!
...
n!: n*(n-1)*(n-2)...*2*1=n*(n-1)!
所以,假设有一个方法factorial(n)用来求n的阶乘,那么n的阶乘还可以表示为n*factorial(n-1)

암호:

public class Test {
    
    
	public static void main(String[] args) throws Exception {
    
    
		int result = factorial(5);
		System.out.println(result);
	}
	
	public static int factorial(int n){
    
    
		if (n==1){
    
    
			return 1;
		}
		return n*factorial(n-1);
	}
}

2.2.2 병합 정렬

병합정렬은 분할 정복(Divide and Conquer) 방식의 가장 대표적인 응용인 병합 연산(Merge Operation)을 기반으로 한 효과적인 정렬 알고리즘이다. 정렬된 하위 시퀀스를 결합하여 완전히 정렬된 시퀀스를 얻습니다. 즉, 먼저 각 하위 시퀀스를 순서대로 만든 다음 하위 시퀀스 세그먼트를 순서대로 만듭니다. 두 개의 정렬된 목록을 하나의 정렬된 목록으로 병합하는 것을 양방향 병합이라고 합니다.

요구 사항:
정렬 전: {8,4,5,7,1,3,6,2}
정렬 후: {1,2,3,4,5,6,7,8}

분류 원리:

  1. 데이터 집합을 가능한 한 요소가 동일한 두 개의 하위 그룹으로 나누고 분할 후 각 하위 그룹의 요소 수가 1이 될 때까지 각 하위 그룹을 계속 분할합니다.
  2. 두 개의 인접한 하위 그룹을 정렬된 큰 그룹으로 병합합니다.
  3. 마지막에 하나의 그룹만 남을 때까지 2단계를 계속 반복합니다.

여기에 이미지 설명 삽입
병합 정렬 API 디자인:

여기에 이미지 설명 삽입
병합 원칙:
여기에 이미지 설명 삽입
병합 정렬 코드 구현:

//排序代码
public class Merge {
    
    
	private static Comparable[] assist;//归并所需要的辅助数组
	/*
	对数组a中的元素进行排序
	*/
	public static void sort(Comparable[] a) {
    
    
		assist = new Comparable[a.length];
		int lo = 0;
		int hi = a.length-1;
		sort(a, lo, hi);
	}
	/*
	对数组a中从lo到hi的元素进行排序
	*/
	private static void sort(Comparable[] a, int lo, int hi) {
    
    
		if (hi <= lo) {
    
    
			return;
		}
		int mid = lo + (hi - lo) / 2;
		//对lo到mid之间的元素进行排序;
		sort(a, lo, mid);
		//对mid+1到hi之间的元素进行排序;
		sort(a, mid+1, hi);
		//对lo到mid这组数据和mid到hi这组数据进行归并
		merge(a, lo, mid, hi);
	}
	/*
	对数组中,从lo到mid为一组,从mid+1到hi为一组,对这两组数据进行归并
	*/
	private static void merge(Comparable[] a, int lo, int mid, int hi) {
    
    
		//lo到mid这组数据和mid+1到hi这组数据归并到辅助数组assist对应的索引处
		int i = lo;//定义一个指针,指向assist数组中开始填充数据的索引
		int p1 = lo;//定义一个指针,指向第一组数据的第一个元素
		int p2 = mid + 1;//定义一个指针,指向第二组数据的第一个元素
		//比较左边小组和右边小组中的元素大小,哪个小,就把哪个数据填充到assist数组中
		while (p1 <= mid && p2 <= hi) {
    
    
			if (less(a[p1], a[p2])) {
    
    
				assist[i++] = a[p1++];
			} else {
    
    
				assist[i++] = a[p2++];
			}
		}
		//上面的循环结束后,如果退出循环的条件是p1<=mid,则证明左边小组中的数据已经归并完毕,如果退
		出循环的条件是p2<=hi,则证明右边小组的数据已经填充完毕;
		//所以需要把未填充完毕的数据继续填充到assist中,//下面两个循环,只会执行其中的一个
		while(p1<=mid){
    
    
			assist[i++]=a[p1++];
		}
		while(p2<=hi){
    
    
			assist[i++]=a[p2++];
		}
		//到现在为止,assist数组中,从lo到hi的元素是有序的,再把数据拷贝到a数组中对应的索引处
		for (int index=lo;index<=hi;index++){
    
    
			a[index]=assist[index];
		}
	}
	/*
	比较v元素是否小于w元素
	*/
	private static boolean less(Comparable v, Comparable w) {
    
    
		return v.compareTo(w) < 0;
	}
	/*
	数组元素i和j交换位置
	*/
	private static void exch(Comparable[] a, int i, int j) {
    
    
		Comparable t = a[i];
		a[i] = a[j];
		a[j] = t;
	}
}

//测试代码
public class Test {
    
    
	public static void main(String[] args) throws Exception {
    
    
		Integer[] arr = {
    
    8, 4, 5, 7, 1, 3, 6, 2};
		Merge.sort(arr);
		System.out.println(Arrays.toString(arr));
	}
}

병합 정렬 시간 복잡도 분석:

병합 정렬은 분할정복 사고의 가장 대표적인 예로서, 위의 알고리즘에서 a[lo...hi]를 정렬하고 먼저 a[lo...mid]와 a[mid+1.. .hi] 두 부분, 재귀 호출을 통해 별도로 정렬되고 마지막으로 정렬된 하위 배열이 최종 정렬 결과로 병합됩니다. 이 재귀의 출구는 배열이 더 이상 두 개의 하위 배열로 나눌 수 없으면 병합을 위해 병합이 실행되고 병합할 때 요소의 크기를 판단하여 정렬한다는 것입니다.
여기에 이미지 설명 삽입

덴드로그램을 사용하여 병합을 설명합니다. 배열에 8개의 요소가 있는 경우 가장 작은 하위 배열을 찾기 위해 매번 2로 나눕니다. 총 분할 수는 3이므로 트리에는 3개의 레이어가 있습니다. 그런 다음 위에서 log8부터 bottom k 계층에는 2^k 하위 배열이 있고 각 배열의 길이는 2^(3-k)이며 병합에는 최대 2^(3-k)비교가 필요합니다. 따라서 각 레이어의 비교 횟수는 이고 2^k * 2^(3-k)=2^33 레이어의 합계는 입니다 3*2^3.

요소의 개수가 n이라고 가정하면 병합 정렬을 이용한 분할 개수는 log2(n)이므로 총 log2(n)개의 레이어가 있고, log2(n)을 사용하여 위의 3개 레이어의 개수를 대체하고, 그리고 최종 3*2^3병합 정렬의 시간 복잡도는 다음과 같습니다. log2(n)* 2^(log2(n))=log2(n)*n, big O 파생 규칙에 따라 기본을 무시하고 최종 병합 정렬의 시간 복잡도는 입니다 O(nlogn).

병합 정렬의 단점:
배열 공간을 추가로 신청해야 하므로 공간 복잡도가 높아져 공간을 시간과 교환하는 일반적인 작업입니다.

병합 정렬 및 힐 정렬 성능 테스트:
힐 정렬의 성능이 삽입 정렬에 의한 것임을 테스트를 통해 알 수 있습니다 이제 병합 정렬을 배웠으니 병합 정렬과 힐 정렬 중 어느 것이 더 효율적인가요? 동일한 테스트 방법을 사용하여 두 정렬 알고리즘 간의 성능 비교를 완료합니다.

1000000에서 1로 역 데이터를 저장하는 reverse_arr.txt 파일을 사용하면 이 데이터 배치를 기반으로 테스트를 완료할 수 있습니다. 테스트 아이디어: 정렬을 수행하기 전 시간을 기록하고 정렬이 완료된 후 시간을 기록합니다. 두 시간의 시간 차이가 정렬 시간을 소모합니다.

힐 정렬 및 삽입 정렬 성능 비교 테스트 코드:

public class SortCompare {
    
    
	public static void main(String[] args) throws Exception{
    
    
		ArrayList<Integer> list = new ArrayList<>();
		//读取a.txt文件
		BufferedReader reader = new BufferedReader(new InputStreamReader(new
		FileInputStream("reverse_merge_shell.txt")));
		String line=null;
		while((line=reader.readLine())!=null){
    
    
			//把每一个数字存入到集合中
			list.add(Integer.valueOf(line));
		}
		reader.close();
		//把集合转换成数组
		Integer[] arr = new Integer[list.size()];
		list.toArray(arr);
		// testMerge(arr);//使用归并排序耗时:1200
		testShell(arr);//使用希尔排序耗时:1277
	}
	public static void testMerge(Integer[] arr){
    
    
		//使用插入排序完成测试
		long start = System.currentTimeMillis();
		Merge.sort(arr);
		long end= System.currentTimeMillis();
		System.out.println("使用归并排序耗时:"+(end-start));
	}
	public static void testShell(Integer[] arr){
    
    
		//使用希尔排序完成测试
		long start = System.currentTimeMillis();
		Shell.sort(arr);
		long end = System.currentTimeMillis();
		System.out.println("使用希尔排序耗时:"+(end-start));
	}
}

테스트를 통해 대량의 데이터 배치를 처리할 때 Hill 정렬과 병합 정렬 간에 큰 차이가 없음을 확인했습니다.

2.3 퀵 정렬

퀵 정렬은 버블 정렬을 개선한 것입니다. 기본 아이디어는 다음과 같습니다. 정렬할 데이터를 원패스 정렬로 두 개의 독립적인 부분으로 나누고 한 부분의 모든 데이터가 다른 부분의 모든 데이터보다 작은 다음 두 부분에 대해 빠른 처리를 수행합니다. 이 정렬 방법에 따른 데이터는 전체 정렬 프로세스를 재귀적으로 수행할 수 있으므로 전체 데이터가 정렬된 시퀀스가 ​​됩니다.

요구 사항:
정렬 전: {6, 1, 2, 7, 9, 3, 4, 5, 8}
정렬 후: {1, 2, 3, 4, 5, 6, 7, 8, 9}

분류 원리:

  1. 먼저 배열을 왼쪽과 오른쪽 부분으로 나누는 컷오프 값을 설정합니다.
  2. 컷오프 값보다 크거나 같은 데이터는 배열의 오른쪽에, 컷오프 값보다 작은 데이터는 배열의 왼쪽에 놓습니다. 이때 왼쪽 부분의 각 요소는 컷오프 값보다 작거나 같고 오른쪽 부분의 각 요소는 컷오프 값보다 크거나 같습니다.
  3. 그런 다음 왼쪽 및 오른쪽 데이터를 독립적으로 정렬할 수 있습니다. 왼쪽 배열 데이터의 경우 경계값을 취하여 데이터의 이 부분을 왼쪽과 오른쪽 부분으로 나눌 수 있으며 더 작은 값은 왼쪽에, 큰 값은 오른쪽에 배치됩니다. 오른쪽의 배열 데이터도 유사하게 처리할 수 있습니다.
  4. 위의 과정을 반복하면 이것이 재귀적 정의임을 알 수 있습니다. 왼쪽 부분을 재귀적으로 정렬한 후 오른쪽 부분의 순서를 재귀적으로 정렬합니다. 왼쪽과 오른쪽 부분의 데이터가 정렬되면 전체 배열의 정렬이 완료됩니다.

여기에 이미지 설명 삽입
빠른 정렬 API 디자인:
여기에 이미지 설명 삽입

분할 원칙:
배열을 두 개의 하위 배열로 나누는 기본 아이디어:

  1. 참조 값을 찾고 두 개의 포인터를 사용하여 배열의 헤드와 테일을 각각 가리킵니다.
  2. 먼저 꼬리에서 머리까지 기준값보다 작은 요소를 검색하고 검색이 발견되면 중지하고 포인터의 위치를 ​​기록합니다.
  3. 그런 다음 머리에서 꼬리까지 기준 값보다 큰 요소를 검색하고 검색이 발견되면 중지하고 포인터의 위치를 ​​기록합니다.
  4. 현재 왼쪽 포인터 위치와 오른쪽 포인터 위치에서 요소를 교환합니다.
  5. 왼쪽 포인터의 값이 오른쪽 포인터의 값보다 클 때까지 2, 3, 4단계를 반복하고 중지합니다.

여기에 이미지 설명 삽입
빠른 정렬 코드 구현:

//排序代码
public class Quick {
    
    
	public static void sort(Comparable[] a) {
    
    
		int lo = 0;
		int hi = a.length - 1;
		sort(a, lo,hi);
	}
	private static void sort(Comparable[] a, int lo, int hi) {
    
    
		if (hi<=lo){
    
    
			return;
		}
		//对a数组中,从lo到hi的元素进行切分
		int partition = partition(a, lo, hi);
		//对左边分组中的元素进行排序
		//对右边分组中的元素进行排序
		sort(a,lo,partition-1);
		sort(a,partition+1,hi);
	}
	
	public static int partition(Comparable[] a, int lo, int hi) {
    
    
		Comparable key=a[lo];//把最左边的元素当做基准值
		int left=lo;//定义一个左侧指针,初始指向最左边的元素
		int right=hi+1;//定义一个右侧指针,初始指向左右侧的元素下一个位置
		//进行切分
		while(true){
    
    
			//先从右往左扫描,找到一个比基准值小的元素
			while(less(key,a[--right])){
    
    //循环停止,证明找到了一个比基准值小的元素
				if (right==lo){
    
    
					break;//已经扫描到最左边了,无需继续扫描
				}
			}
			//再从左往右扫描,找一个比基准值大的元素
			while(less(a[++left],key)){
    
    //循环停止,证明找到了一个比基准值大的元素
				if (left==hi){
    
    
					break;//已经扫描到了最右边了,无需继续扫描
				}
			}
			if (left>=right){
    
    
				//扫描完了所有元素,结束循环
				break;
			}else{
    
    
				//交换left和right索引处的元素
				exch(a,left,right);
			}
		}
		//交换最后rigth索引处和基准值所在的索引处的值
		exch(a,lo,right);
		return right;//right就是切分的界限
	}
	/*
	数组元素i和j交换位置
	*/
	private static void exch(Comparable[] a, int i, int j) {
    
    
		Comparable t = a[i];
		a[i] = a[j];
		a[j] = t;
	}
	/*
	比较v元素是否小于w元素
	*/
	private static boolean less(Comparable v, Comparable w) {
    
    
		return v.compareTo(w) < 0;
	}
}


//测试代码
public class Test {
    
    
	public static void main(String[] args) throws Exception {
    
    
		Integer[] arr = {
    
    6, 1, 2, 7, 9, 3, 4, 5, 8};
		Quick.sort(arr);
		System.out.println(Arrays.toString(arr));
	}
}

퀵 정렬과 병합 정렬의 차이점:
퀵 정렬은 배열을 두 개의 하위 배열로 나누고 두 부분이 독립적으로 정렬되는 또 다른 분할 정복 정렬 알고리즘입니다. 퀵 정렬과 병합 정렬은 상호 보완적입니다. 병합 정렬은 배열을 두 개의 하위 배열로 나누어 별도로 정렬하고 정렬된 하위 배열을 병합하여 전체 배열을 정렬하는 반면, 빠른 정렬 방법은 두 배열을 모두 정렬할 때 전체 배열은 자연스럽게 정렬됩니다. 병합 정렬은 배열을 반으로 나누어 전체 배열이 처리되기 전에 병합 호출이 발생하고, 퀵 정렬은 배열 내용에 따라 분할 배열의 위치가 달라지며 배열 전체가 처리된 후 재귀 호출이 발생합니다. 처리됩니다.

퀵정렬의 시간복잡도 분석 :
퀵정렬의 분할은 양 끝에서 시작하여 좌우가 일치할 때까지 교대로 탐색하므로 분할 알고리즘의 시간복잡도는 O(n)이나 전체 퀵정렬의 시간복잡도는 분할 수는 관련이 있습니다.

최적의 상황: 각 분할에 대해 선택된 벤치마크 번호는 현재 시퀀스를 동일한 부분으로 나눕니다.

빠른 정렬의 최적 경우
여기에 이미지 설명 삽입
배열의 분할을 트리로 본다면 위의 그림은 최적의 경우를 나타낸 그림으로, 최적의 경우 퀵 정렬의 시간복잡도는 O이다. (nlogn);

최악의 경우: 각 분할에 대해 선택된 참조 번호는 현재 시퀀스에서 가장 크거나 작은 숫자이므로 각 분할이 하위 그룹을 갖게 되므로 총 n번 분할해야 하므로 최악의 경우 시간 복잡도 퀵 정렬은 O(n^2)입니다.

최악의 경우 퀵 정렬:
여기에 이미지 설명 삽입

평균의 경우: 각 분할에 대해 선택된 참조 번호가 최대값과 최소값, 중간값이 아닌 경우에도 수학적 귀납법을 사용하여 빠른 정렬의 시간 복잡도가 O(nlogn)임을 증명할 수 있습니다. 수학적 귀납법은 수학 관련 지식이 많아 혼동하기 쉬우므로 여기에서는 평균 사례의 시간 복잡도를 증명하지 않겠습니다.

2.4 선별의 안정성

안정성의 정의:
배열 arr에는 여러 요소가 있으며 그 중 A 요소와 B 요소가 같고 A 요소가 B 요소 앞에 있습니다.정렬 알고리즘을 사용하여 정렬하면 다음과 같을 수 있습니다. A 요소가 여전히 B 요소 앞에 있음을 보장합니다. 이 알고리즘은 안정적이라고 말할 수 있습니다.

여기에 이미지 설명 삽입
안정성의 중요성:
데이터 집합이 한 번만 정렬되면 안정성은 일반적으로 의미가 없으며 데이터 집합이 여러 번 정렬되어야 하는 경우 안정성이 의미가 있습니다. 예를 들어 정렬할 콘텐츠가 상품 오브젝트의 그룹인데, 1차 정렬은 가격이 낮은 것부터 높은 순으로 정렬되고, 2차 정렬은 판매량이 높은 것부터 낮은 순으로 정렬된다. 두 번째 정렬, 동일한 판매량을 달성할 수 있습니다 개체는 여전히 높은 가격과 낮은 가격의 순서로 표시되며 판매량이 다른 개체만 재정렬해야 합니다. 이렇게 하면 첫 번째 정렬의 원래 의미를 유지할 수 있고 시스템 오버헤드를 줄일 수 있습니다.

첫 번째는 낮은 가격에서 높은 가격 순으로 정렬됩니다.
여기에 이미지 설명 삽입
두 번째는 높은 가격에서 낮은 가격 순으로 판매 기준으로 정렬됩니다.
여기에 이미지 설명 삽입
일반적인 정렬 알고리즘의 안정성:

버블 정렬:
arr[i]>arr[i+1]일 때만 요소의 위치가 바뀌고, 같을 때는 위치가 바뀌지 않으므로 버블 정렬은 안정적인 정렬 알고리즘입니다.

선택 정렬:
선택 정렬은 각 위치에 대해 가장 작은 현재 요소를 선택하는 것입니다. 예를 들어 데이터 {5(1), 8, 5(2), 2, 9}가 있는 경우 첫 번째 패스에서 선택된 가장 작은 요소는 2이므로 5(1)은 2와 위치를 교환하게 됩니다. 이때 5(1)은 5(2)보다 뒤쳐져 안정성이 파괴되므로 선택 정렬은 불안정한 정렬 알고리즘입니다.

삽입정렬 :
정렬된 순서의 끝부터 비교를 시작하여 삽입할 요소를 이미 정렬된 가장 큰 요소와 비교하여 크면 바로 뒤에 삽입하고 그렇지 않으면 삽입한다. 찾을 때까지 앞으로 이동합니다. 삽입 위치입니다. 삽입된 요소와 동일한 요소가 발견되면 삽입할 요소가 동일한 요소 뒤에 배치됩니다. 따라서 동일한 요소의 순서는 변경되지 않았으며 원래 순서가 없는 시퀀스의 순서가 정렬된 순서이므로 삽입 정렬이 안정적입니다.

힐 정렬(Hill Sorting):
힐 정렬은 서로 다른 스텝 길이에 따라 요소를 삽입하고 정렬하는 것입니다. 삽입 정렬은 안정적이고 동일한 요소의 상대적 순서를 변경하지 않지만 다른 삽입 정렬 프로세스에서 동일한 요소가 각각의 삽입에 있을 수 있습니다. 정렬 이동, 그리고 마지막으로 안정성이 중단되므로 Hill 정렬이 불안정합니다.

병합 정렬:
병합하는 과정에서 병합 정렬은 arr[i]<arr[i+1]인 경우에만 위치를 교환하며, 두 요소가 같으면 위치가 교환되지 않으므로 안정성이 손상되지 않습니다. 병합 정렬이 안정적입니다.

퀵소트 :
퀵소트는 기준값이 필요하고, 기준값 우측에서 기준값보다 작은 요소를 찾고, 기준값 좌측에서 기준값보다 큰 요소를 찾아 이 두 요소를 교환한다. , 현재로서는 불안정하므로 퀵 정렬은 불안정한 알고리즘입니다.

Supongo que te gusta

Origin blog.csdn.net/qq_33417321/article/details/121956391
Recomendado
Clasificación