기사 디렉토리
다음 정렬은 작은 정렬부터 큰 정렬까지를 예로 들어 설명합니다.
1. 직접 삽입 정렬
시간 복잡도:
최선의 경우: 완전히 정렬됨 1 2 3 4 5 O(N)
최악의 경우: 완전히 역전됨 5 4 3 2 1 O(N^2) (산술 시퀀스 합산과 같음) 공간 복잡도:
O(1) 안정성
:
안정적 주어진 데이터가 더 정렬된 경우 직접 삽입 정렬이 더 빠름
기본적으로 정렬된 데이터 집합이 있는 경우 직접 삽입 정렬을 사용하는 것이 좋습니다.
public static void insertSort(int[] array) {
for(int i = 1; i < array.length; i++) {
int j = i - 1;
int tmp = array[i];
for(; j >= 0; j--) {
//跳出循环有两种情况,1.j<0 2.array[j]<=tmp
if(array[j] > tmp) {
//如果条件变为array[j]>=tmp,则排序变为不稳定
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = tmp;//每次把tmp插入数组后,都会重新获取i和j的位置
}
}
2. 힐 정렬
Hill Sorting은 Direct Insertion Sorting의 최적화로, Jumping grouping은 가능한 한 작은 요소를 앞으로 이동시킬 수 있음
몇 개의 증분(갭이 얼마나 되는가)을 그룹으로 나누는가
시간복잡도: N^1.3 ~ N^1.5
공간복잡도: O(1)
안정성: 불안정
public static void shellSort(int[] array) {
int gap = array.length;//增量为多少(gap为多少),则被分为多少组
while(gap > 1) {
//里面已经包括了排序gap为1的情况
gap /= 2;
shell(array, gap);
}
}
private static void shell(int[] array, int gap) {
for(int i = gap; i < array.length; i++) {
//i+=gap也行,因为gap最后会变为1
int j = i - gap;
int tmp = array[i];
for(; j >= 0; j -= gap) {
//跳出循环有两种情况,1.j<0 2.array[j]<=tmp
if(array[j] > tmp) {
//如果条件变为array[j]>=tmp,则排序变为不稳定
array[j + gap] = array[j];
}else {
break;
}
}
array[j + gap] = tmp;//每次把tmp插入数组后,都会重新获取i和j的位置
}
}
3. 직접 선택 정렬
시간 복잡도: 상황이 좋든 나쁘든 다음 두 가지 쓰기 방법은 O(N^2)(연산 시퀀스의 합과 같음) 공간
복잡도: O(1)
안정성: 불안정
public static void selectSort1(int[] array){
for(int i = 0; i < array.length; i++) {
int minIndex = i;
for(int j = i + 1; j < array.length; j++) {
if(array[j] < array[minIndex]) {
minIndex = j;//在所有的j下标元素中找比array[minIndex]还要小的元素的下标
}
}
swap(array, i, minIndex);
}
}
private static void swap(int[]array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//写法二
public static void selectSort2(int[] array){
int left = 0;
int right = array.length - 1;
while(left < right) {
//与写法一相比将写法一最外层的for循环改为了while循环
int minIndex = left;
int maxIndex = left;//maxIndex一定是left,因为下面的j下标是从left+1开始从前往后遍历
for(int i = left + 1; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;//在所有的j下标元素中找比array[minIndex]还要小的元素的下标
}
if(array[i] > array[maxIndex]) {
maxIndex = i;//在所有的j下标元素中找比array[maxIndex]还要大的元素的下标
}
}
swap(array, left, minIndex);
//当最大值原来刚好在最小值的位置(left位置)时,则上一步已经将最小值与left交换,此时最大值在原来最小值的位置,
if(maxIndex == left) {
//所以要做这一步操作
maxIndex = minIndex;
}
swap(array, right, maxIndex);
left--;
right++;
}
}
4. 힙 정렬
시간복잡도: O(N)(대형 루트 힙 생성) + O(N logN)(힙의 각 노드 하향 조정) 또는 O(N logN)
공간복잡도: O(1)
안정성: 불안정
데이터 양이 매우 많을 때 Hill 정렬의 시간복잡도는 N^1.3 ~ N^1.4이고, Heap 정렬은 대수, Hill 정렬은 지수함수이기 때문에 Heap 정렬이 Hill 정렬보다 빨라야 함
public static void heapSort(int[] array){
creatBigHeap(array);
int end = array.length - 1;
while(end > 0) {
swap(array, 0, end);//因为是大堆,堆顶的元素最大,将堆顶元素与队尾元素交换,使队尾元素变成队内最大元素
shiftDown(array, 0, end);//在这里end=array.length-1,而在shiftDown中end代表数组的总长度,
end--; //相当于去掉队尾元素再进行向下调整
}
}
private static void creatBigHeap(int[] array) {
for(int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(array, parent, array.length);
}
}
private static void shiftDown(int[] array, int parent, int end) {
int child = 2 * parent + 1;
while(child < end) {
//因为end传的是array.length,所以条件用<而不用<=
if(child + 1 < end && array[child] < array[child + 1]) {
child++;
}
if(array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
다섯, 버블 정렬
시간 복잡도: O(N^2) 최적화 후 최상의 경우(한 번만 비교)는 O(N)
공간 복잡도: O(1)
안정성: 안정적
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length - 1; i++) {
boolean flg = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if(array[j] > array[j + 1]) {
swap(array, j, j + 1);
flg = true;
}
}
if(!flg) {
return;
}
}
}
여섯, 빠른 정렬
퀵 정렬(벤치마크를 루트로 하여 이진 트리를 생성하는 것과 같음) 시간
복잡도:
최선의 경우: O(N*logN) 완전 이진 트리/완전 이진 트리
최악의 경우: O(N^2)(산술 시퀀스의 합과 같음) 단일 분기 트리 공간 복잡도: 최선의 경우: O
(
logN) 완전 이진 트리/완전 이진 트리의 높이 최악의 경우: O
(N) 단일 분기 트리가 N개의 노드를 생성 안정성
: 불안정
세 가지 벤치마킹 방법: Hoare 방식 , 파기 방식, 정방향 및 역방향 포인터 방식
6.1 재귀적 퀵 정렬
여기에서 재귀적으로 구현된 퀵정렬은 두 가지 최적화가 있는데, 첫 번째 최적화는 3개 숫자의 중간을 취하여 원래 벤치마크를 찾는 방법을 사용하여 생성된 트리가 더 완전한 이진 트리에 가까워 트리의 높이를 줄이고 공간 복잡도를 줄이는 것입니다. 재귀
횟수
public static void quickSort(int[] array){
quickSortF(array, 0, array.length - 1);//参数right传的是array.length-1
}
private static void quickSortF(int[] array, int left, int right) {
if(left >= right) return;//这是递归的结束条件,有=,因为left=right时不用再创建节点了,这个节点已经有序
//递归到后面的较小区间时用插入法
if(right - left + 1 <= 7) {
//随着快速排序的进行,整个数据正在趋于有序,当区间越来越小且区间的内容越来越有序时,
insertSort1(array, left, right);//这部分区间可以用插入排序,这样做可以减少递归的次数
return;
}
//三数取中法,降低了二叉树的高度,降低了空间复杂度,使空间复杂度变为O(logn)
int midIndex = midOfTree(array, left, right);
swap(array, midIndex, left);//交换完之后保证left下标是三个数中中间大的数字
int pivot = partition2(array, left, right);//找到一次基准相当于排好了当前基准这个元素在数组中的顺序,找到基准后,基准左边都是比基准小的,基准右边都是比基准大的
//先递归左边,递归完左边再递归右边
quickSortF(array, left, pivot - 1);
quickSortF(array, pivot + 1, right);
}
//插入法
private static void insertSort1(int[] array, int left, int right) {
for(int i = left + 1; i <= right; i++) {
int j = i - 1;
int tmp = array[i];
for(; j >= left; j--) {
//跳出循环有两种情况,1.j<0 2.array[j]<=tmp
if(array[j] > tmp) {
//如果条件变为array[j]>=tmp,则排序变为不稳定
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = tmp;//每次把tmp插入数组后,都会重新获取i和j的位置
}
}
//在三数中找到中间大小数的下标
private static int midOfTree(int[] array, int left, int right) {
int mid = (left + right) / 2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] > array[left]) {
return left;
}else if(array[mid] < array[right]) {
return right;
}else {
return mid;
}
}
}
//三种找基准的方法,在排序的过程中序列可能会不一样,但最终排好序的结果一样,建议优先使用挖坑法,right参数传的都是array.length-1
//Hoare法找基准
private static int partition1(int[] array, int left, int right) {
int key = array[left];
int i = left;//将基准的下标保存到i中,因为后面要用left与right相交位置的元素与基准进行交换,这里的交换函数只传下标
while(left < right) {
while (left < right && array[right] >= key) {
//left<right这个条件很重要,因为right不断--,此时再判断array[right]>=key时array[right]有可能会越界
right--;//先走right,因为一开始基准为left,先走right,left和right相遇的地方才能是比基准小的,才能把比基准小的与基准交换放在基准前
}
while (left < right && array[left] <= key) {
//array[left]<=key或上面array[right]>=key的=一定要有,否则可能会进入死循环(如当left和right的值相同时)
left++;
}
swap(array, left, right);
}
swap(array, left, i);
return left;
}
//挖坑法找基准
private static int partition2(int[] array, int left, int right) {
int key = array[left];
while(left < right) {
while (left < right && array[right] >= key) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= key) {
left++;
}
array[right] = array[left];
}
array[left] = key;
return left;
}
//前后指针法找基准
private static int partition3(int[] array, int left, int right) {
int prev = left;
int cur = left + 1;
while(cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {
//cur在前面找比基准小的,prev保持在cur找到比基准小的之后要跟prev交换的那个位置,prev是前置++
swap(array, prev, cur);//代码走到这说明cur和prev拉开了距离且cur<left,prev>=left
}
cur++;//array[cur]>=array[left]时cur直接往前走,与prev拉开距离
}
swap(array, prev, left);
return prev;
}
6.2 비재귀적 퀵 정렬
public static void quickSortNor(int[] array) {
Stack<Integer> stack = new Stack<>();
int left = 0;
int right = array.length - 1;
int piovt = partition2(array, left, right);
if(piovt - 1 > left) {
stack.push(left);//往栈上先放left后放right,则出栈时先出right后出left
stack.push(piovt - 1);
}
if(piovt + 1 < right) {
stack.push(piovt + 1);
stack.push(right);
}
while(!stack.isEmpty()) {
//因为这里出栈时先出right后出left,根据出出来的right和left找新的基准,所以相当于先递归二叉树右边,再递归二叉树左边
right = stack.pop();
left = stack.pop();
piovt = partition2(array, left, right);
if(piovt - 1 > left) {
stack.push(left);
stack.push(piovt - 1);
}
if(piovt + 1 < right) {
stack.push(piovt + 1);
stack.push(right);
}
}
}
일곱, 병합 정렬
시간 복잡도: O(N*logN) 머지 정렬은 머지 과정에서 각 레이어를 N번 순회해야 함, 총 logN 레이어
공간 복잡도: O(N) 병합 정렬은 마지막에 머지할 때 원래 배열과 정확히 같은 크기의 추가 배열을 적용해야 함 병합 정렬의 단점은 공간 복잡도가 너무 크다 안정성 :
안정적
.
7.1 병합 정렬의 재귀적 구현
public static void mergeSort(int[] array) {
mergeSort(array, 0, array.length - 1);//参数right传的是array.length-1
}
private static void mergeSort(int[] array, int left, int right) {
if(left >= right) return;
int mid = (left + right) / 2;
//分裂左边
mergeSort(array, left, mid);
//分裂右边
mergeSort(array, mid + 1, right);
//合并,在合并的过程中进行了排序
merge(array, left, right, mid);
}
private static void merge(int[] array, int left, int right, int mid) {
int s1 = left;
int s2 = mid + 1;
int[] tmpArr = new int[right - left + 1];//申请一个新的数组,大小为right-left+1
int k = 0;
while(s1 <= mid && s2 <= right) {
//满足这个循环条件说明s1~mid和s2~right这两个区间都同时有数据
if(array[s2] < array[s1]) {
//若条件改为array[s2]<=array[s1]则排序变为不稳定
tmpArr[k++] = array[s2++];
}else {
tmpArr[k++] = array[s1++];
}
}
while(s1 <= mid) {
tmpArr[k++] = array[s1++];
}
while(s2 <= right) {
tmpArr[k++] = array[s2++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i + left] = tmpArr[i];//将tmpArr数组拷回原来数组时,赋给array[i+left],因为原来数组的left有可能不是0
}
}
7.2 병합 정렬의 비재귀적 구현
병합하기 전에 그룹의 수는 계속 감소하고 각 요소 그룹은 계속 증가합니다. 하나씩 그룹으로 (간격이 1) 다음 두 개와 두 그룹으로 (간격이 2) 병합하기 전에 중간 및 오른쪽이 경계를 넘을 수 있으며 이때 조정이 필요합니다
.
public static void mergeSortNor(int[] array) {
int gap = 1;
while(gap < array.length) {
for (int i = 0; i < array.length; i += 2*gap) {
int left = i;
int mid = left + gap - 1;
int right = mid + gap;
if(mid >= array.length) {
//mid有可能会越界,越界时要做出调整
mid = array.length - 1;
}
if(right >= array.length) {
//right有可能会越界,越界时要做出调整
right = array.length - 1;
}
merge(array, left, right, mid);
}
gap *= 2;
}
}
8. 계산 및 분류
시간 복잡도: O(2N+데이터 범위) 또는 O(MAX(N, 데이터 범위))
공간 복잡도: O(데이터 범위)
안정성: 안정적이나 다음 코드로 구현된 카운팅 정렬이 불안정함
카운팅 정렬은 특정 간격으로 집중된 데이터를 정렬하는 데 적합함
public static void countSort(int[] array) {
int minVal = array[0];
int maxVal = array[0];
//求数组中的最大值和最小值
for (int i = 0; i < array.length; i++) {
if(array[i] < minVal) {
minVal = array[i];
}
if(array[i] > maxVal) {
maxVal = array[i];
}
}
int[] count = new int[maxVal - minVal + 1];//依据最大值和最小值来确定计数数组的大小
//遍历原来的数组进行计数
for (int i = 0; i < array.length; i++) {
count[array[i] - minVal]++;//计数数组的下标相当于要排序数组的元素内容
}
//遍历count,把当前元素写回array
int index = 0;
for (int i = 0; i < count.length; i++) {
while(count[i] != 0) {
array[index] = i + minVal;
index++;
count[i]--;
}
}
}