基本思想:所谓交换,就是根据序列中两个记录的键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。 1、冒泡排序 1、对前n个元素从头到尾两两进行比较,如果第一个比第二个大,就交换他们两个,最后得到的最后一个元素就是前n个元素的最大值。 2、然后对前n-1个、前n-2个…,一直到前两个元素进行第一步操作,依次得到他们里面的最大值。 3、进行n-1次第一步的操作后就将元素排序好了。 最差时间复杂度:O(n^2) 平均时间复杂度:O(n^2) 空间复杂度:O(1) 稳定性:稳定(相同的元素排序后相对位置不改变)
#include <stdio.h> #include <iostream> using namespace std; void bubbleSort(int arr[], int n) { int tmp = 0; for (int i = 0; i < n - 1; ++i) { for (int j = 0; j < n - i - 1; ++j) { if (arr[j] > arr[j+1]) { tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } } } int main() { int arr[] = {12, 2, 34, 7, 81, 52, 13, 6, 9}; int n = sizeof(arr) / sizeof(arr[0]); bubbleSort(arr, n); for (int i = 0; i < n; ++i) { cout << arr[i] << " "; } cout << endl; return 0; }冒泡排序优化1: 原来的冒泡排序可能在某次循环中已经将顺序排好了,那之后的排序就是多余的,我们可以用一个isSorted标记来判断本次循环中有没有元素交换,没有的话说明已经排好序了,后面的循环就没必要继续下去了。
void bubbleSort(int arr[], int n) { int tmp = 0; for (int i = 0; i < n - 1; ++i) { bool isSorted = true; for (int j = 0; j < n - i - 1; ++j) { if (arr[j] > arr[j+1]) { tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; isSorted = false; } } if (isSorted) return; } }冒泡排序优化2: 按照原有的逻辑,有序区的长度和排序的轮数是相等的。比如第一轮排序过后的有序区长度是1,第二轮排序过后的有序区长度是2 … 实际上,数列真正的有序区可能会大于这个长度,比如上图中仅仅第二轮,后面5个元素实际都已经属于有序区。因此后面的许多次元素比较是没有意义的。 如何避免这种情况呢?我们可以在每一轮排序的最后,记录下最后一次元素交换的位置,那个位置也就是无序数列的边界,再往后就是有序区了。
void bubbleSort(int arr[], int n) { int tmp = 0; //记录最后一次交换的位置 int lastExchangeIndex = 0; //无序数列的边界,每次比较只需比较到这里为止 int sortBorder = n - 1; for (int i = 0; i < n - 1; ++i) { bool isSorted = true; for (int j = 0; j < sortBorder; ++j) { if (arr[j] > arr[j+1]) { tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; isSorted = false; lastExchangeIndex = j; } } sortBorder = lastExchangeIndex; if (isSorted) return; } }2、快速排序 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上为止。快排的真谛在于 极端情况下每次将概率等分 1/2 每次小于这个数的放在前面 大于的放在后面 即每次排序都找出了一个正确位置,使得下一次排序个数减少一半,类似于二分法。
将区间按照基准值划分为左右两半部分的常见方式有:
hoare版本挖坑法前后指针版本最差时间复杂度:O(n^2) 平均时间复杂度:O(n*logn) 空间复杂度:O(1) 稳定性:稳定(相同的元素排序后相对位置不改变)
#include <iostream> #include <utility> #include <stack> using namespace std; #define M 5 //hoare版本(交换) int _Partition_1(int *ar, int left, int right)//划分函数 { int key = ar[left];//将第一个元素作为基准值 while (left < right)//left和right的变化原则就是每次移动后比较的基准永远不变 { while (left < right && ar[right] >= key) --right; swap(ar[left], ar[right]); while (left < right && ar[left] < key) ++left; swap(ar[left], ar[right]); } return left;//返回下一次调用时的位置 } void QuickSort(int *ar, int left, int right) { if (left >= right) return; if (right - left + 1 <= M) { InsertSort_1(ar, left, right);//当数据量比较小时采用插入排序 } else { int pos = _Partition_1(ar, left, right); QuickSort(ar, left, pos - 1); QuickSort(ar, pos + 1, right); } } //挖坑法(覆盖) int _Partition_2(int *ar, int left, int right) { int key = ar[left]; while (left < right) { while (left<right && ar[right] >= key) --right; ar[left] = ar[right];//不调用交换函数,因为基准值已经被key记录了 while (left<right && ar[left]<key) ++left; ar[right] = ar[left]; } ar[left] = key; return left; } void QuickSort(int *ar, int left, int right) { if (left >= right) return; if (right - left + 1 <= M) { InsertSort_1(ar, left, right);//当数据量比较小时采用插入排序 } else { int pos = _Partition_2(ar, left, right); QuickSort(ar, left, pos - 1); QuickSort(ar, pos + 1, right); } }对于快排,它的基准并不好找,比如说在序列有序的情况下(1,2,3,4,5…)每次比较后不会使得下一次排序的数量减少一半,时间复杂度退化为O(n^2)。 在基准理想情况下,时间复杂度是O(NlogN),这里的n和logn, 分别代表了调用栈的高度,和完成每层的时间,在a取数组的第一项时候,是最糟的情况,完成每层需要的时间是n,栈的高度是n,时间复杂度就是n²,当取中间值的时候,完成每层的时间是n,但是调用栈的高度变成了logn,所以这个时候时间复杂度是nlogn。
//三数取中法选key int GetMidIndex(int *ar, int left, int right)//返回left、right、mid的中间值 { int mid = (left + right) / 2; int a = ar[left], b = ar[mid], c = ar[right]; if ((a>=b && a<=c) || (a<=b && a>=c)) return left; else if ((b>=a && b<=c) || (b<=a && b>=c)) return mid; else return right; } //前后指针版本 //将一个中间值作为基准值,可以将数组划分的更均匀 int _Partition_3(int *ar, int left, int right) { int index = GetMidIndex(ar, left, right); if (index != left)//将数组的第一个元素与mid、left、right的中间值交换 swap(ar[index], ar[left]); int key = ar[left]; //pos来记录比较过的元素中小于key的最右边的元素 //i来记录正在比较的元素 int pos = left; for (int i = left + 1; i <= right; ++i) { if (ar[i] < key) { ++pos; if (pos != i) { swap(ar[pos], ar[i]); } } } swap(ar[left], ar[pos]); return pos; } void QuickSort(int *ar, int left, int right) { if (left >= right) return; if (right - left + 1 <= M) { InsertSort_1(ar, left, right);//当数据量比较小时采用插入排序 } else { int pos = _Partition_3(ar, left, right); QuickSort(ar, left, pos - 1); QuickSort(ar, pos + 1, right); } } //快速排序非递归实现 void QuickSortNonR(int *ar, int left, int right) { stack<int> st;//栈用来存放每次划分的下标 st.push(left); st.push(right); while (!st.empty()) { int begin = 0, end = 0, pos = 0; end = st.top(); st.pop(); begin = st.top(); st.pop(); pos = _Partition_1(ar, begin, end); if (begin < pos - 1) { st.push(begin); st.push(pos-1); } if (pos + 1 < end) { st.push(pos+1); st.push(end); } } }1、直接选择排序 1、在未排序序列中找到最小元素,存放到待排序序列的起始位置。 2、从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。 3、以此类推,直到所有元素均排序完毕。 最差时间复杂度:O(N^2) 平均时间复杂度:O(n^2) 空间复杂度:O(1) 稳定性:不稳定
#include <iostream> using namespace std; void selectionSort(int arr[], int n) { int temp = 0, minIndex = 0; for (int i = 0; i < n - 1; ++i) { minIndex = i; for (int j = i + 1; j < n; ++j) { if (arr[j] < arr[minIndex]) minIndex = j; } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } int main() { int arr[] = {12, 2, 34, 7, 81, 52, 13, 6, 9}; int n = sizeof(arr) / sizeof(arr[0]); selectionSort(arr, n); for (auto e : arr) cout << e << " "; cout << endl; return 0; }理解选择排序的不稳定性: 举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。 2、堆排序 最差时间复杂度:O(nlogn) 平均时间复杂度:O(nlogn) 空间复杂度:O(1) 稳定性:不稳定 堆排序(Heapsort)是指利用堆积数(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆(大根堆,根节点元素最大),排降序建小堆。堆积数是一个近似完全二叉树的结构,并同时满足堆的性质:子节点的键值或索引总是小于(或者大于)它的父节点。 算法思想: 1、将初始待排序序列(R1,R2…Rn)构建成大顶堆,此堆为初始的无序堆; 2、将堆顶元素R[1]与最后一个元素R[n]交换,此时此时得到新的无序区(R1,R2,…,Rn-1)和新的有序区(Rn),且满足R[1,2,…,n-1]<=R[n]; 3、由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,…,Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2,…,Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程一直到有序区的元素个数为n-1,则整个排序过程完成。
#include <iostream> using namespace std; void _AdjustDown(int *ar, int left, int right, int start)//调整为大堆 { //要注意我们的start是按照left为0传入的 int i = start + left;//left可能不是0,这个时候最后一个节点的父节点需要加left int j = 2 * start +1 + left;//j和i一个意思,都加left while (j <= right)//左子节点存在 { if (j+1<=right && ar[j]<ar[j+1]) ++j;//j指向左右子节点值较大子节点 if (ar[i] < ar[j]) { swap(ar[i], ar[j]); i = j;//改变调整位置 //j的求法就是先把i还原为0做开始元素下标时的值,然后和0情况一样,只是加一个left j = 2 * (i-left) + 1 + left; } else break; } } void HeapSort(int *ar, int left, int right) { int n = right - left + 1;//待排序元素个数 int start = n/2 - 1;//将序列看成一颗完全二叉树,找到最后一个节点的父节点 while (start >= 0)//调整为大堆 { _AdjustDown(ar, left, right, start); --start; } int end = right;//无序序列的最后一个元素 while (end > left) { swap(ar[left], ar[end]); --end; _AdjustDown(ar, left, end, 0);//再次调整为大堆(只调整根节点) } } int main() { int ar[] = {100,6,2,4,9,7,6,8,10,25,14}; HeapSort(ar, 3, sizeof(ar)/sizeof(ar[0])-1); for (auto e : ar) cout << e << " "; cout << endl; return 0; }我们需要注意的是:排序的序列不一定是从数组第一个元素开始的,所以要考虑在二叉树中元素下标的偏移left。其实很简单,调整方法和left为0是一样的,区别只是我们无法按照left为0时的下标规律来找到二叉树中的某个元素了。将序列调整为堆无非就是能够找到我们要调整的元素的下标,然后就可以按规则调整了,我们可以先按照left为0的规律求下标,然后再把求出来的下标加上偏移量left就可以找到调整堆所需元素的下标了。
1、直接插入排序 (1) 从第一个元素开始,该元素认为已经被排序。 (2) 取出下一个元素,在已经排序的元素序列中从后向前扫描。 (3) 如果该元素(已排序)大于新元素,将该元素移到下一位置。 (4) 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。 (5) 将新元素插入到该位置后重复步骤2~5。 最差时间复杂度:O(n^2) 平均时间复杂度:O(n^2) 空间复杂度:O(1) 稳定性:稳定
#include <iostream> using namespace std; void insertSort(int arr[], int n) { for (int i = 1; i < n; ++i) { //若第i个元素大于第i-1个元素,不需要查找,直接插入 //小于的话,查找正确位置后插入 int j = i - 1; int x = arr[i]; //查找在有序表的插入位置 while (j>=0 && x<arr[j]) { arr[j+1] = arr[j]; --j; } arr[j+1] = x;//插入到正确位置 } } int main() { int arr[] = {12, 2, 34, 7, 81, 52, 13, 6, 9}; int n = sizeof(arr) / sizeof(arr[0]); insertSort(arr, n); for (auto e : arr) cout << e << " "; cout << endl; return 0; }2、 希尔排序( 缩小增量排序 ) 希尔排序也称递减增量排序算法,是插入排序的一种更高效的改进版本。 平均时间复杂度: O(NlogN) 最差时间复杂度: O(N^2) 空间复杂度:O(1) 稳定性:不稳定
void _ShellSort(int *ar, int left, int right, int gap) { int i = 0; for(i = left; i <= right - gap; ++i)//这里的插入排序是多个序列混排的 { if(ar[i+gap] < ar[i])//防止已经比较过的元素重复比较,不加也行 { int end = i; int tmp = ar[end + gap]; while(end >= left && tmp < ar[end])//插入排序 { ar[end + gap] = ar[end]; end -= gap; } ar[end+gap] = tmp; } } } void ShellSort(int *ar, int left, int right) { int i = 0; int dlta[] = {5, 3, 2, 1}; int n = sizeof(dlta) / sizeof(dlta[0]); for(i = 0; i < n; ++i) { _ShellSort(ar, left, right, dlta[i]); } }最差时间复杂度:O(NlogN) 平均时间复杂度:O(NlogN) 空间复杂度:O(N) 稳定性:稳定
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 算法思想: 1、把长度为n的输入序列分成两个长度为n/2的子序列; 2、对这两个子序列分别采用归并排序; 3、将两个排序好的子序列合并成一个最终的排序序列。
#include <iostream> using namespace std; void _MergeSort(int *ar, int left, int right, int *temp) { if (left >= right) return; int mid = (left+right) / 2; _MergeSort(ar, left, mid, temp); _MergeSort(ar, mid+1, right, temp); int begin1 = left, end1 = mid; int begin2 = mid + 1, end2 = right; int i = 0; while (begin1<=end1 && begin2<=end2) { if (ar[begin1] <= ar[begin2]) temp[i++] = ar[begin1++]; else temp[i++] = ar[begin2++]; } while (begin1 <= end1) temp[i++] = ar[begin1++]; while (begin2 <= end2) temp[i++] = ar[begin2++]; memcpy(ar+left, temp, sizeof(int)*(right-left+1)); } void MergeSort(int *ar, int left, int right) { int n = right - left + 1; int *temp = new int[n]; _MergeSort(ar, left, right, temp); free(temp); temp = nullptr; } int main() { int ar[] = {10, 6, 7, 1, 3, 9, 4, 2}; MergeSort(ar, 3, sizeof(ar)/sizeof(ar[0])-1); for (auto e : ar) cout << e << " "; cout << endl; return 0; }