1、算法的伪代码(这样便于理解):
INSERTION-SORT (A, n) A[1 . . n]
for j ←2 to n
do key ← A[ j]
i ← j – 1
while i > 0 and A[i] > key
do A[i+1] ← A[i]
i ← i – 1
A[i+1] = key
2、思想:如下图所示,每次选择一个元素K插入到之前已排好序的部分A[1…i]中,插入过程中K依次由后向前与A[1…i]中的元素进行比较。若发现发现A[x]>=K,则将K插入到A[x]的后面,插入前需要移动元素。
3、算法时间复杂度。
最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n)
最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n^2)
平均情况下:O(n2)
4、稳定性。
理解性记忆比死记硬背要好。因此,我们来分析下。稳定性,就是有两个相同的元素,排序先后的相对位置是否变化,主要用在排序时有多个排序规则的情况下。在插入排序中,K1是已排序部分中的元素,当K2和K1比较时,直接插到K1的后面(没有必要插到K1的前面,这样做还需要移动!!),因此,插入排序是稳定的。
5、代码实现
/* 对于一个int数组,请编写一个插入排序算法,对数组元素排序。 给定一个int数组A及数组的大小n,请返回排序后的数组。 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] */ #include <iostream> #include <cstdlib> using namespace std; /*直接插入排序*/ int *IntertionSort(int *A, int n) { if (A == NULL || n <= 0) return A; /*将序列中的i=1~i=n-1的元素依次选择合适位置插入*/ for (int i = 1; i < n; ++i) { /*要插入的元素*/ int tmp = A[i]; /*从已有序序列尾向前寻找合适位置*/ int j = i - 1; for (; j >= 0; --j) { if (A[j] > tmp) A[j + 1] = A[j]; else break; }//for A[j + 1] = tmp; } return A; }
1、思想:希尔排序也是一种插入排序方法,实际上是一种分组插入方法。先取定一个小于n的整数d1作为第一个增量,
把表的全部记录分成d1个组,所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序;
然后,取第二个增量d2(<d1),重复上述的分组和排序,直至所取的增量dt=1(dt<dt-1<…<d2<d1),即所有记录放在
同一组中进行直接插入排序为止。
例如:将 n 个记录分成 d 个子序列:
{ R[0], R[d], R[2d],…, R[kd] }
{ R[1], R[1+d], R[1+2d],…,R[1+kd] }
…
{ R[d-1],R[2d-1],R[3d-1],…,R[(k+1)d-1] }
说明:d=5 时,先从A[d]开始向前插入,判断A[d-d],然后A[d+1]与A[(d+1)-d]比较,如此类推,这一回合
后将原序列分为d个组。<由后向前>
2、时间复杂度。
最好情况:由于希尔排序的好坏和步长d的选择有很多关系,因此,目前还没有得出最好的步长如何选择
(现在有些比较好的选择了,但不确定是否是最好的)。所以,不知道最好的情况下的算法时间复杂度。
最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。
平均情况下:O(N*logN)
3、稳定性。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序
过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)
4、代码
/* 对于一个int数组,请编写一个希尔排序算法,对数组元素排序。 给定一个int数组A及数组的大小n,请返回排序后的数组。 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] */ #include <iostream> #include <cstdlib> using namespace std; /*希尔排序*/ void ShellSort(int *A, int n) { if (A == NULL || n <= 0) return; /*设置增量*/ int d = n; while (d > 1) { d = (d + 1) / 2; for (int i = 0; i < n - d; ++i) { if (A[i + d] < A[i]) { int tmp = A[i + d]; A[i + d] = A[i]; A[i] = tmp; }//if }//for }//while }
1、基本思想:通过无序区中相邻记录关键字间的比较和位置的交换,使关键字最小的记录如气泡一般逐渐往上“漂浮”直至“水面”。
2、时间复杂度
最好情况下:正序有序,则只需要比较n次。故,为O(n)
最坏情况下: 逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(N*N)
3、稳定性
排序过程中只交换相邻两个元素的位置。因此,当两个数相等时,是没必要交换两个数的位置的。所以,它们的相对位置并没有改变,冒泡排序算法是稳定的!
4、代码
/* 对于一个int数组,请编写一个冒泡排序算法,对数组元素排序。 给定一个int数组A及数组的大小n,请返回排序后的数组。 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] */ #include <iostream> #include <cstdlib> using namespace std; int *BubbleSort(int *A, int n) { if (n <= 0) return A; /*进行n-1趟冒泡*/ for (int i = 0; i < n - 1; ++i) { /*每次冒泡针对头到n-i-1尾比较工作*/ for (int j = 0; j < n - i - 1; ++j) { if (A[j] > A[j + 1]) { int tmp = A[j + 1]; A[j + 1] = A[j]; A[j] = tmp; }//if }//for }//for return A; }
1、思想:它是由冒泡排序改进而来的。在待排序的n个记录中任取一个记录(通常取第一个记录),把该记录放入适当位置后,数据序列被此记录划分成两部分。所有关键字比该记录关键字小的记录放置在前一部分,所有比它大的记录放置在后一部分,并把该记录排在这两部分的中间(称为该记录归位),这个过程称作一趟快速排序。
说明:最核心的思想是将小的部分放在左边,大的部分放到右边,实现分割。
2、算法复杂度
最好的情况下:因为每次都将序列分为两个部分(一般二分都复杂度都和logN相关),故为 O(N*logN)
最坏的情况下:基本有序时,退化为冒泡排序,几乎要比较N*N次,故为O(N*N)
3、稳定性
由于每次都需要和中轴元素交换,因此原来的顺序就可能被打乱。如序列为 5 3 3 4 3 8 9 10 11会将3的顺序打乱。所以说,快速排序是不稳定的!
4、代码
<span style="font-size:12px;">/* 对于一个int数组,请编写一个归并排序算法,对数组元素排序。 给定一个int数组A及数组的大小n,请返回排序后的数组。 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] */ #include <iostream> #include <cstdlib> #include <ctime> #include <stack> using namespace std; /*划分函数*/ int partition(int *A, int left, int right); /*递归实现*/ void QuickSort(int *A, int left, int right); /*非递归实现*/ void QuickSort2(int *A, int left, int right); void swap(int &a, int &b) { int tmp = a; a = b; b = a; } int partition(int *A, int left, int right) { ///*随机选择*/ srand((int)time(NULL)); int pos = left + (rand() % (right - left + 1)); ///*交换*/ swap(A[pos], A[right]); /*设置左侧小于等于区间{},初始化为left左侧*/ int lessPos = left - 1; int pivot = A[right]; for (int i = left; i < right; ++i) { if (A[i] <= pivot) { /*交换,并小于等于区间尾部右移*/ ++lessPos; swap(A[lessPos], A[i]); }//if } swap(A[lessPos + 1], A[right]); return lessPos + 1; } /*快速排序*/ int *QuickSort(int *A, int n) { if (A == NULL || n <= 0) return A; QuickSort2(A, 0, n-1); return A; } /*快速排序的递归实现*/ void QuickSort(int *A, int left, int right) { if (left < right) { int pos = partition(A, left, right); QuickSort(A, left, pos - 1); QuickSort(A, pos + 1, right); }//if } /*快速排序的非递归实现*/ void QuickSort2(int *A, int left, int right) { stack<int> st; if (left < right) { int mid = partition(A, left, right); if (left < mid - 1) { st.push(left); st.push(mid - 1); }//if if (right > mid + 1) { st.push(mid + 1); st.push(right); }//if /*其实就是用栈保存每一个待排序子串的首尾元素下标, 下一次while循环时取出这个范围,对这段子序列进行partition操作*/ while (!st.empty()) { int rhs = st.top(); st.pop(); int lhs = st.top(); st.pop(); mid = partition(A, lhs, rhs); if (lhs < mid - 1) { st.push(lhs); st.push(mid - 1); }//if if (rhs > mid + 1) { st.push(mid + 1); st.push(rhs); }//if }//while }//if } int main() { int arr[] = { 1, 2, 3, 5, 2, 3 }; QuickSort(arr, 6); for (int i = 0; i < 6; ++i) cout << arr[i] << "\t"; cout << endl; system("pause"); return 0; }</span>
1、思想:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。具体做法是:选择最小的元素与未排序部分的首部交换,使得序列的前面为有序。
2、时间复杂度。
最好情况下:交换0次,但是每次都要找到最小的元素,因此大约必须遍历N*N次,因此为O(N*N)。减少了交换次数!
最坏情况下,平均情况下:O(N*N)
3、稳定性
由于每次都是选取未排序序列A中的最小元素x与A中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的!
4、代码
#include <iostream> #include <cstdlib> using namespace std; int *SelectionSort(int *A, int n) { if (n <= 0) return A; /*进行n-1趟选择*/ for (int i = 0; i < n - 1; ++i) { /*每次选择,找到i到n-1处最小元素位置,放在i位置*/ int minPos = i; for (int j = i+1; j < n; ++j) { if (A[j] < A[minPos]) { minPos = j; }//if }//for /*将最小元素放在此趟选择的首位*/ int tmp = A[i]; A[i] = A[minPos]; A[minPos] = tmp; }//for return 0; }
1、思想:利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或者最小)的记录。也就是说,以最小堆为例,根节点为最小元素,较大的节点偏向于分布在堆底附近。
2、算法复杂度
最坏情况下,接近于最差情况下:O(N*logN),因此它是一种效果不错的排序算法。
3、稳定性
堆排序需要不断地调整堆,因此它是一种不稳定的排序!
4、代码
#include <iostream> #include <time.h> #include <cstdlib> #define N 10 using namespace std; //声明建大顶堆函数 void BuildMaxHeap(int * array); //声明堆排序函数 void HeapSort(int * array); //声明调整为大顶堆函数 void MaxHeapify(int * array, int n); //返回堆的数据个数 int HeapSize; int main() { //声明一个待排序数组 int array[N]; //设置随机化种子,避免每次产生相同的随机数 srand(time(0)); for (int i = 0; i < N; i++) { array[i] = rand() % 101;//数组赋值使用随机函数产生1-100之间的随机数 } cout << "排序前:" << endl; for (int j = 0; j < N; j++) { cout << array[j] << " "; } cout << endl << "排序后:" << endl; //调用堆排序函数对该数组进行排序 HeapSort(array); for (int k = 0; k < N; k++) { cout << array[k] << " "; } cout << endl; system("pause"); return 0; } void HeapSort(int * array) { BuildMaxHeap(array); for (int i = N - 1; i >= 0; i--)//数组中下标从0 - N-1 { int temp = array[0]; array[0] = array[i]; array[i] = temp; HeapSize -= 1; MaxHeapify(array, 1);//在堆中,堆顶元素下标从1开始 } } void BuildMaxHeap(int * array) { HeapSize = N; for (int i = N / 2; i >= 1; i--)//注意i的取值,堆的高度从1 - N/2 { MaxHeapify(array, i); } } void MaxHeapify(int * array, int temp) { int largest;//以temp为顶点的子树的堆顶 int l = 2 * temp;//求以temp为顶点的子树左儿子 int r = 2 * temp + 1;//求以temp为顶点的子树右儿子 if (l <= HeapSize && array[l - 1] > array[temp - 1])//首先判断左儿子是否存在,即l<=HeapSize { largest = l; } else{ largest = temp; } if (r <= HeapSize && array[r - 1] > array[largest - 1])//首先判断右儿子是否存在,即r<=HeapSize { largest = r; } if (largest != temp) { int t = array[temp - 1]; array[temp - 1] = array[largest - 1]; array[largest - 1] = t; MaxHeapify(array, largest);//调整为大顶堆 } }
1、思想:多次将两个或两个以上的有序表合并成一个新的有序表。
2、算法时间复杂度
最好的情况下:一趟归并需要n次,总共需要logN次,因此为O(N*logN)
最坏的情况下,接近于平均情况下,为O(N*logN)
说明:对长度为n的文件,需进行logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。
3、稳定性
归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。
4、缺点是,它需要O(n)的额外空间。但是很适合于多链表排序。
5、代码
/* 对于一个int数组,请编写一个归并排序算法,对数组元素排序。 给定一个int数组A及数组的大小n,请返回排序后的数组。 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] */ #include <iostream> #include <cstdlib> using namespace std; void MergeSort(int *A, int left, int right); void Merge(int *A, int left, int mid, int right); int* MergeSort(int *A, int n) { if (A == NULL || n <= 0) return A; MergeSort(A, 0, n - 1); return A; } void MergeSort(int *A, int left, int right) { if (left >= right) return; int mid = (left + right) / 2; MergeSort(A, left, mid); MergeSort(A, mid + 1, right); Merge(A, left, mid, right); } /*将排序后的left~mid与mid+1~right两个子序列合并*/ void Merge(int *A, int left, int mid, int right) { int *tmp = new int[right - left + 1]; /*合并排序后的元素到tmp临时数组*/ int lhs = left, rhs = mid + 1, k = 0; while (lhs <= mid && rhs <= right) { if (A[lhs] <= A[rhs]) tmp[k++] = A[lhs++]; else tmp[k++] = A[rhs++]; }//while /*直接拷贝剩余元素*/ while (lhs <= mid) { tmp[k++] = A[lhs++]; }//while while (rhs <= right) { tmp[k++] = A[rhs++]; }//while /*拷贝临时数组数据到原数组*/ for (int i = left; i <= right; ++i) { A[i] = tmp[i - left]; }//for delete[]tmp; } //int main() //{ // int arr[] = { 1, 2, 3, 5, 2, 3 }; // MergeSort(arr, 6); // // for (int i = 0; i < 6; ++i) // cout << arr[i] << "\t"; // cout << endl; // system("pause"); // return 0; //}
1、思想:它是一种非比较排序。它是根据位的高低进行排序的,也就是先按个位排序,然后依据十位排序……以此类推。示例如下:
2、算法的时间复杂度
分配需要O(n),收集为O(r),其中r为分配后链表的个数,以r=10为例,则有0~9这样10个链表来将原来的序列分类。而d,也就是位数(如最大的数是1234,位数是4,则d=4),即"分配-收集"的趟数。因此时间复杂度为O(d*(n+r))。
3、稳定性
基数排序过程中不改变元素的相对位置,因此是稳定的!
4、适用情况:如果有一个序列,知道数的范围(比如1~1000),用快速排序或者堆排序,需要O(N*logN),但是如果采用基数排序,则可以达到O(4*(n+10))=O(n)的时间复杂度。算是这种情况下排序最快的!!