您的位置:首页 > 理论基础 > 数据结构算法

数据结构-排序算法总结

2017-07-22 23:13 253 查看
数据结构中排序算法是各大公司面试的常客,下面则是对各种排序算法的总结:

基本概念

1、排序:排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列

2、内部排序和外部排序:整个排序过程完全在内存中进行,叫做内部排序。数据量较大需要借助外部存储设备才能完成,叫做外部排序。

3、时间复杂度:算法中基本操作重复执行的次数称为算法的时间复杂度,记为T(n)=O(f(n))。

4、空间复杂度:算法所需存储空间的量度称为算法的空间复杂度,记为S(n)=O(f(n)),若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间为O(1)。

5、排序的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

算法分类

排序算法总共可以分4大类,分别是插入类排序、交换类排序、选择类排序、归并类排序。各种排序算法的分类以及时间复杂度和空间复杂度的对比图如下:



下面将对各种排序算法逐一进行叙述:

一、插入类排序

思想:在一个已经排好序的序列中,将未被排进的元素按照原先的规定插入到指定位置。

1、 直接插入排序:

思想:最基本的插入排序,将第i个元素插入到前i−1个元素中的适当位置。

时间复杂度:T(n)=O(n2)

空间复杂度:S(n)=O(1)

稳定性:稳定排序

Java实现:

// 直接插入排序
// 时间复杂度:T(n)=O(n^2)
// 空间复杂度:S(n) = O(1)
// 稳定性:稳定排序
public void directInsertSort(int[] arr){
int temp; //哨兵
int i,j;
for(i = 1;i < arr.length;i++){
if(arr[i] < arr[i-1]){
temp = arr[i];
for( j = i-1;j >= 0 && arr[j] > temp;j--)
arr[j+1] = arr[j];
arr[j+1] = temp;
}
}
}


2、 折半插入排序:

思想:折半插入排序是直接插入排序的改进版,由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。由于折半查找只是减少了比较次数,但是元素的移动次数不变,因此时间复杂度和直接插入排序一样大。

时间复杂度:T(n)=O(n2)

空间复杂度:S(n)=O(1)

稳定性:稳定排序

Java实现:

// 折半插入排序
// 时间复杂度:T(n)=O(n^2)
// 空间复杂度:S(n) = O(1)
// 稳定性:稳定排序
public void binaryInsertSort(int[] arr) {
int length = arr.length;
int temp;
for(int i = 1;i < length;i++){
int midIndex = (0 + i-1)/2;
if(arr[i] < arr[midIndex]){
for(int j = 0;j <= midIndex;j++){
if(arr[i] < arr[j]){
temp   = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
else {
for (int j = midIndex; j < i; j++) {
if (arr[i] < arr[j]) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
}


3、 希尔排序:

思想:希尔排序是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序把待排序序列分成若干较小的子序列,然后逐个使用直接插入排序法排序,最后再对一个较为有序的序列进行一次排序,主要是为了减少移动的次数,提高效率。

时间复杂度:T(n)=O(n32)

空间复杂度:S(n)=O(1)

稳定性:不稳定排序

Java实现:

// 希尔排序
// 时间复杂度:T(n)=O(n^1.5)
// 空间复杂度:S(n) = O(1)
// 稳定性:不稳定排序
public void shellSort(int[] arr) {
int temp,i,j;
//增量gap,并逐步缩小
// 增量
for(int gap = arr.length/2;gap > 0;gap /= 2){
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for(i = gap;i < arr.length;i++){
for(j = i-gap;j >= 0 && arr[j] > arr[j+gap];j -= gap){
temp = arr[j];
arr[j] = arr[j+gap];
arr[j+gap] = temp;
}
}
}
}


二、交换类排序

思想:两两比较待排序记录的关键字,发现两记录的次序相反时即进行交换,直到没有反序的记录为止。

1、 冒泡排序:

思想:冒泡排序将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮。

时间复杂度:T(n)=O(n2)

空间复杂度:S(n)=O(1)

稳定性:稳定排序

Java实现:

// 冒泡排序
// 时间复杂度:T(n)=O(n^2)
// 空间复杂度:S(n) = O(1)
// 稳定性:稳定排序
public void bubbleSort(int[] arr) {
int length = arr.length;
int temp;
for(int i = 0;i < length;i++){
for(int j = 1;j < length;j++){
if(arr[j] < arr[j-1]){
temp     = arr[j];
arr[j]   = arr[j-1];
arr[j-1] = temp;
}
}
}
}


2、 快速排序:

思想:冒泡排序一次只能消除一个逆序,为了能一次消除多个逆序,采用快速排序,因此快速排序是冒泡排序的一种改进。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。首先在序列中随机选取一个数字,将大于该数字的放在其右边,小于该数字的放在左边。对于该数字左、右边的序列递归调用该过程。

时间复杂度:T(n)=O(n∗log(n))

空间复杂度:S(n)=O(log(n))

稳定性:不稳定排序

Java实现:

/*
// 快速排序
// 时间复杂度:T(n)=O(nlogn)
// 空间复杂度:S(n) = O(logn)
// 稳定性:不稳定排序
*/
void quickSort(int arr[], int start, int end) {
if(start < end){//判断start是否等于end,如果等于则说明各区间只有一个数
int i = start;
int j = end;
int x = arr[start];//将start作为基准数
while(i < j){//如果i=j,则结束
while(i < j && arr[j] >= x) // 从右向左找第一个小于x的数
j--;
//此时,arr[j] < x,则令arr[i]=arr[j],且i向前移动一格
if(i < j)
arr[i++] = arr[j];

while(i < j && arr[i] < x)// 从左向右找第一个大于等于x的数
i++;
//此时,arr[i] >= x,则令arr[j]=arr[i],且j向后移动一格
if(i < j)
arr[j--] = arr[i];
}
//此时,arr[i]=arr[j]=x,且比x大的数全在它的右边,小于或等于它的数全在其左边。下面则再对其左右区间重复这个步骤
arr[i] = x;
quickSort(arr,start,i-1);//对其左边区间重复上述操作
quickSort(arr,i+1,end);//对其右边区间重复上述操作
}
}


三、选择类排序

思想:选择排序的基本思想是对待排序的记录序列进行n-1遍的处理,第i遍处理是将L[i..n]中最小者与L[i]交换位置。

1、 简单选择排序:

思想:依次遍历序列,每次选出值最小的记录,并和当前所遍历的下标最小的记录进行对换。

时间复杂度:T(n)=O(n2)

空间复杂度:S(n)=O(1)

稳定性:稳定排序

Java实现:

/*
*简单选择排序
* // 时间复杂度:T(n)=O(n^2)
// 空间复杂度:S(n) = O(1)
// 稳定性:稳定排序
*/
void selectSort(int arr[]) {
int length = arr.length;
int temp;
for(int i = 0;i < length;i++){
int min = arr[i];
for(int j = i;j < length;j++){
if(min > arr[j]){
temp = min;
min = arr[j];
arr[j] = temp;
}
}
arr[i] = min;
}
}


2、 堆排序:

思想:堆排序与快速排序,归并排序一样都是时间复杂度为 O(n*logn)的排序方法。想明白什么是堆排序,得先明白什么是二叉堆:

二叉堆是完全二叉树或者是近似完全二叉树。 二叉堆满足二个特性:

1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。

2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的 键值总是小于或等于任何一个子节点的键值时为最小堆。

一般都用数组来表示堆,i 结点的父结点下标就为(i – 1) / 2。它的左右子结点下 标分别为 2 * i + 1 和 2 * i + 2。



堆的插入删除



由于堆也是用数组模拟的,由于堆的根节点是最小的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。

时间复杂度:T(n)=O(nlogn)

空间复杂度:S(n)=O(1)

稳定性:不稳定排序

Java实现:

/**
* 堆排序
*/
void heapSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
buildMaxHeap(array);
for (int i = array.length - 1; i >= 0; i--) {
int temp = array[0];
array[0] = array[i];
array[i] = temp;
maxHeap(array, i, 0);
}
}

/**
* 创建最大堆
*/
void buildMaxHeap(int[] array) {
if (array == null || array.length <= 1) {
return;
}
for (int i = array.length / 2; i >= 0; i--) {
maxHeap(array, array.length, i);
}
}

/**
* 调整最大堆
*/
void maxHeap(int[] array, int heapSize, int index) {
int left = index * 2 + 1;
int right = index * 2 + 2;

int largest = index;
if (left < heapSize && array[left] > array[index]) {
largest = left;
}

if (right < heapSize && array[right] > array[largest]) {
largest = right;
}

if (index != largest) {
int temp = array[index];
array[index] = array[largest];
array[largest] = temp;
maxHeap(array, heapSize, largest);
}
}


四、归并排序:

1、 归并排序:

思想:该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组的组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。

时间复杂度:T(n)=O(nlogn)

空间复杂度:S(n)=O(n)

稳定性:稳定排序

Java实现:

/**
* 归并排序
* 时间复杂度:T(n)=O(nlogn)
* 空间复杂度:S(n) = O(n)
* 稳定性:稳定排序
*/
void mergeSort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左边
mergeSort(nums, low, mid);
// 右边
mergeSort(nums, mid + 1, high);
// 左右归并
merge(nums, low, mid, high);
}
}

void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;

// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}

// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}

// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}

// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息