排序(冒泡、插排、选择、桶排、希尔、堆、归并、快排) C++

排序(冒泡、插排、选择、桶排、希尔、堆、归并、快排) C++

前提说明:
排序可以是升序,可以是降序,可以从后往前排,可以从前往后排,结果正确即可,本文以升序为主。

随机数组生成

void randomArr(int arr[], int n)
{
	for (int i = 0; i < n; i++)
	{
         arr[i] = i;
	}
	// 如果你要产生0~99这100个整数中的一个随机整数,可以表达为:int num = rand() % 100; 
	for (int i = 0; i < n; i++)
	{
         std::swap(arr[i], arr[rand()%n]);
	}
}

辅助打印函数(数组)

void printArr(int arr[], int n)
{
    for(int i=0; i<n; i++)
    {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

辅助打印函数(vector)// 重载

void printArr(std::vector<int> vec)
{
    for(int i=0; i<vec.size(); i++)
    {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;
}

一、冒泡

思想:两两比较,顺序不交换,逆序前后交换

  1. 冒泡冒泡,十分的形象的描述,每一次都将最大的数,吹到待排序的最后一个位置(或者将最小的数放在第一个位置);
  2. n 个数最多 n-1 趟冒泡;
  3. 每一趟,前后两数比较,大的去后面,小的去前面;
  4. i 控制排序趟数,j 控制比较的两个数;
时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N)O(N^2)O(N^2)O(1)稳定
void bubbleSort(int arr[], int n)// 数组传的就是引用,n 是长度
{
    for(int i=0; i<n-1; i++)
    {
        for(int j=0; j<n-i-1; j++)
        {
            if(arr[j] > arr[j+1])
            {
                std::swap(arr[j], arr[j+1]);
            }
        }
    }
}

冒泡plus
设置一个 flag ,当某一趟没有交换的情况下,排序完成

void bubbleSortPlus(int arr[], int n)// 数组传的就是引用,n 是长度
{
    for(int i=0; i<n-1; i++)
    {
        bool flag = true;
        for(int j=0; j<n-i-1; j++)
        {
            if(arr[j] > arr[j+1])
            {
                std::swap(arr[j], arr[j+1]);
                flag = false;
            }
        }
        if(flag)
        {
            break ;
        }
    }
}

二、直接插入

思想:

  1. 数组分为,有序部分和待排序部分,开始有序部分为第一个元素,第二个往后都是待排序部分;
  2. 从第二个元素开始;
  3. 先记录自己的 val ;
  4. 和前面的元素依次比较,直到找到 自己小的小的元素,或到达队首;
  5. 如果比自己大,就往后顺一个位置;

注:最多插入次数,n - 1次;
在基本有序和数据量少时,直接插入排序是比较好的选择,效率非常高

时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N)O(N^2)O(N^2)O(1)稳定
void insertSort(int arr[], int n)
{
   for (int i = 0; i < n - 1; i++)
	{
		int j = i + 1;
		int tmp = arr[j];
		while (j > 0)
		{
			// 要用 tmp 去比较,在每趟比较中tmp才是要插入的值,且他不变
			if (tmp >= arr[j - 1])
			{
				break;
			}
			arr[j] = arr[j - 1];
			j--;
		}
		arr[j] = tmp;
	}
}

简化一下内部循环代码:

void insertSort(int arr[], int n)
{
   for (int i = 0; i < n - 1; i++)
	{
		int j = i + 1;
		int tmp = arr[j];
		// 修改循环结束条件
		while (j > 0 && tmp < arr[j - 1])
		{
			arr[j] = arr[j - 1];
			j--;
		}
		arr[j] = tmp;
	}
}

三、希尔排序

直接插入排序的 plus
思想:

  1. n 个数据,分成 gap = n / 2 组数据,对每一组数据进行插入排序;
  2. 缩进 1 / 2,即 gap 组数据,变成 gap /2组数据,在进行插排;
  3. 重复操作 2 ,直到 gap == 1
时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N)O(N^1.3)O(N^2)O(1)不稳定
void shellInsert(int arr[], int start, int gap)
{
	int tmp = arr[start];
	while (start >= gap && tmp < arr[start - gap])
	{
		arr[start] = arr[start - gap];
		start -= gap;
	}
	arr[start] = tmp;
}

void shellSort(int arr[], int n)
{
    // 控制分组
    for(int gap = n/2; gap > 0; gap /= 2)
    {
        // 从start为下标开始的元素,依次插入到合适的位置
        int start = gap;
		while (start < n)
		{
			shellInsert(arr, start, gap);
			start++;
		}
    }
}

四、选择排序

思想:

  1. 从前往后,最多进行 n-1 轮排序;
  2. 每一轮以待排序数组的一个元素值为 min,minIndex记录最小值下标,每次开始为待排序数组的第一个元素的下标,与待排序的其他值进行比较,比 min 小则,maxIndex 修改;
  3. min也要修改 每一轮遍历完,比较 minIndex 和 待排序数组的第一个元素的下标是否相同,相同,不交换,不相同,则交换,进入下一轮交换;

注:
或者紧记录 minIndex 的值,并每次修改,比较时,比较 当前元素和 arr[minIndex] 的值的大小。

时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N^2)O(N^2)O(N^2)O(1)不稳定
void selectSort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++)
	{
         int min = arr[i];
         int minIndex = i;
         for (int j = i + 1; j < n; j++)
         {
             if (arr[j] < min)
             {
                 // 不仅要修改 minIndex 的值,还要修改 min 的大小
                 min = arr[j];
                 minIndex = j;
              }
         }
         if (i != minIndex)
         {
             std::swap(arr[i], arr[minIndex]);
         }
    }
}

简化版:

void selectSort(int arr[], int n)
{
    for (int i = 0; i < n - 1; i++)
	{
         int minIndex = i;
         for (int j = i + 1; j < n; j++)
         {
             if (arr[j] < arr[minIndex])
             {
                 minIndex = j;
              }
         }
         if (i != minIndex)
         {
             std::swap(arr[i], arr[minIndex]);
         }
    }
}

五、堆排序

由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
堆:
性质一:索引为i的左孩子的索引是 (2i+1);
性质二:索引为i的右孩子的索引是 (2i+2);
性质三:索引为i的父结点的索引是 (i-1)/2;
大顶堆:堆中,父节点的值比任一孩子节点的值都大。
小顶堆:堆中,父节点的值比任一孩子节点的值都小。
思想:
选择排序plus

  1. 初始化大顶堆(或小顶堆);
  2. 将堆顶元素和待排序队列最后一个元素交换,队列长度 -1 ,重新调整为大顶堆;
  3. 直到剩下根节点为止;
void heapSort(int arr[], int n)
{
    for(int i = n/2-1; i >= 0; i--)
    {
        heapAdjust(arr, i, n);
    }
    for(int i = n-1; i > 0; i--)
    {
        std::swap(arr[0], arr[i]);
        heapAdjust(arr, 0, i);
    }
}

构建大顶堆:

  1. 从 n / 2,开始,到根节点为止;
  2. 每一次比较父节点和子节点中大的那个节点,小于,则将父节点的位置赋值为子节点大的值,否则结束循环;
  3. 如发生赋值,则继续判断是否子节点部分的大顶堆被破坏,找到父节点值的合适位置。
/*
*第二个参数,起始父节点,第三个参数数组长度
*/
void heapAdjust(int arr[], int father, int n)
{
    int fVal = arr[father];
    for(int i = 2*father+1; i < n; i = 2*father+1)
    {
         if(i<n-1 && arr[i]<=arr[i+1])
        {
            i++;
        }
        if(fVal >= arr[i])
        {
            break;
        }
        arr[father] = arr[i];
        // 不能少,否则陷入死循环
        father = i;
    }
    arr[father] = fVal;
}

注:
// 不能少,否则陷入死循环
father = i;

时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(NlogN)O(NlogN)O(NlogN)O(1)不稳定

六、桶排序(基数排序的特殊)

适用于元素比较紧密的自然数
思想:

  1. 以队列每个元素的 val 作为桶的序号;
  2. 先找到数组的最大值 max,然后生成 max+1 个桶;
  3. 遍历数组,将元素放入桶中,从 0开始,有就++;
  4. 遍历完数组后,遍历桶,给原数组赋值,该桶不为空,将下标赋值给原数组,val–,直到桶遍历完;
int getMAX(int arr[], int n)
{
    int max = 0;
    for(int i=1; i<n; i++)
    {
        if(arr[max] < arr[i])
        {
            max = i;
        }
    }
    return arr[max];
}

void bucketSort(int arr[], int n)
{
    // 错误用法,int[] 数组里面必须是常数
    // int bucket[getMAX(arr, n)+1]{0};
    // int* bucket = new int[getMAX(arr, n) + 1]{0};
	int* bucket = new int[getMAX(arr, n) + 1];
	memset(bucket, 0, sizeof(int)*(getMAX(arr, n) + 1));
    // vector 不用delete
    // std::vector<int> bucket(getMAX(arr, n)+1, 0);
    for(int i=0; i<n; i++)
    {
        bucket[arr[i]]++;
    }
    int i = 0;
    int j = 0;
    while(j < n)
    {
        while(bucket[i]--)
        {
            arr[j++] = i;
        }
        i++;
    }
    delete [] bucket;
    bucket = NULL;
}

学习笔记:
{0} 只能在初始化是使用;
memset 可以在任何时候使用;

时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N)O(N*M)O(N^M)O(M)稳定

注:N为数据个数,M为数据位数

七、归并排序

归并排序是一种占用内存,但却效率高且稳定的算法。
思想:

  1. 将待排序队列进行分解,分解成若干有序队列(单个元素必为有序队列);
  2. 在将每部分的有序队列,两两合并;
  3. 最终重新合并成一个队列;

递归实现(合并的代码在下面)

/*
*递归
*业务代码主要实现函数
*/
void mergeSort(int arr[], int left, int right)
{
	if (left >= right)
	{
         return;
	}
	int mid = (left + right) / 2;
	mergeSort(arr, left, mid);
	mergeSort(arr, mid + 1, right);
	// 合并
	merge(arr, left, mid, right);
}
/*
*主调函数
*/
void mergeSort(int arr[], int n)
{
    // 右边界索引是 n-1
    mergeSort(arr, 0, n-1);
}

非递归实现:

/*
*非递归主要业务逻辑
*/
void mergeSortWhile(int arr[], int len, int n)
{
    int i = 0;
	int mid = i + len - 1;
	int right = i + len * 2 - 1;
	while (right < n)
	{
		merge(arr, i, mid, right);
		// printArr(arr, n);
		i = right+1;
		mid = i + len - 1;
		right = i + len * 2 - 1;
	}
	if (mid < n)
	{
		merge(arr, i, mid, n - 1);
	}
	// printArr(arr, n);
}
/*
* 非递归主调函数
*/
void mergeSortWhile(int arr[], int n)
{
	// len 是每次合并的单个队列长度
	int len = 1;
	while (len < n)
	{
		mergeSortWhile(arr, len, n);
		len *= 2;
	}
}

合并的思路:

  1. 申请待合并队列之和的空间;
  2. 比较两个队列的大小,将值放进去,直到其中一个队列走到尽头;
  3. 处理尾部,将未走到尽头的队列的剩余元素,放入申请的新内存中;
void merge(int arr[], int left, int mid, int right)
{
    int* tmp = (int*)malloc(sizeof(int)*(right - left + 1));
    int l, index, r;
    // index 从0开始,不然可能会越界 l [left, mid], r [mid+1, right]
	index = 0; l = left; r = mid+1;
    // 两个待合并队列,有一个到达队尾就跳出循环
    while(l <= mid && r <= right)
    {
        // 赋值给 tmp
        tmp[index++] = arr[l] <= arr[r] ? arr[l++] : arr[r++];
    }
    while(l <= mid)
    {
        tmp[index++] = arr[l++];
    } 
    while(r <= right)
    {
        tmp[index++] = arr[r++];
    } 
    // 记得重新给队列赋值
    l = left;
    while(l <= right)
    {
        arr[l] = tmp[l - left];
        l++; 
    }
    // 释放内存
    delete [] tmp;
}
时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N*logN)O(N*logN)O(N*logN)O(N+logN)稳定

注:非递归归并的辅助空间,降低到 O(N),因为少了栈空间的迭代。

八、快速排序

前提:三色旗问题
一个数组中,仅有0、1、2三个元素,对他们进行排序。(很显然,我们可以用桶排序,但是,我们不那么做)

思路:

  1. 以 1 为基准,将小于 1 的部分放到左边;
  2. 大于 1 的放到右边;
  3. 等于 1 时,继续遍历下一个元素即可;
/*
*三色旗,荷兰国旗问题
*/
void holandFlag(int arr[], int n)
{
    int l, i, r;
    l = -1; i = 0; r = n;
    while(i < n && i < r)
    {
        if(arr[i] < 1)
        {
            std::swap(arr[++l], arr[i++]);
        }
        else if(arr[i] == 1)
        {
            i++;
        }
        else
        {
            std::swap(arr[i], arr[--r]);
        }
    }
}
/*
*主调函数
*/
int main()
{
    int arr[10]{1, 2, 0, 0, 1, 2, 2, 0, 0, 1};
    int n = sizeof(arr) / sizeof(arr[0]);
    holandFlag(arr, n);
    // 打印方法在片头
    printArr(arr, n);
    return 0;
}

快排思路:

  1. 选取一个基准值,对队列进行划分,分成小于基准值部分的第一部分,等于基准值部分的第二部分和大于基准值的第三部分;
  2. 对第一部分和第三部分,重复 1 的步骤;
  3. 直到第一部分和第三部分只剩下一个元素或者剩下元素都相同为止;

递归实现1(元素不重复时使用):

/*
*用于划分的方法
*left 为左边界索引
*right 为右边界索引
*/
int quickFlag(int arr[], int left, int right)
{
    // 枢轴,枢轴的选取也是一个难点,简便起见,我们这里直接采用左边界为枢轴
    int pivotkey = arr[left];
    while (left < right)
	{
		while (left < right && arr[right] > pivotkey)
		{
			right--;
		}
		std::swap(arr[left], arr[right]);
		while (left < right && arr[left] < pivotkey)
		{
			left++;
		}
		std::swap(arr[left], arr[right]);
	}
	return left;
}
/*
*快排递归函数
*/
void quickSort(int arr[], int left, int right)
{
    // 递归 end condition
    if(left >= right)
    {
        return ;
    }
    int pivot = quickFlag(arr, left, right);
    quickSort(arr, left, pivot-1);
    quickSort(arr, pivot+1, right);
}
/*
*快排递归主函数
*/
void quickSort(int arr[], int n)
{
    quickSort(arr, 0, n-1);
}

递归实现2
划分:

/*
*用于划分的方法
*left 为左边界索引
*right 为右边界索引
*/
std::pair<int, int> quickFlag(int arr[], int left, int right)
{
    int l, i, r;
    // 枢轴,枢轴的选取也是一个难点,简便起见,我们这里直接采用左边界为枢轴
    int pivotkey = arr[left];
    l = left-1; i = left; r = right+1;
    while(i < right+1 && i < r)
    {
        if(arr[i] < pivotkey)
        {
            std::swap(arr[++l], arr[i++]);
        }
        else if(arr[i] == pivotkey)
        {
            i++;
        }
        else
        {
            std::swap(arr[i], arr[--r]);
        }
    }
    return std::make_pair(l, r);
}

主逻辑:

/*
*快排递归函数
*/
void quickSort(int arr[], int left, int right)
{
    // 递归 end condition
    if(left >= right)
    {
        return ;
    }
    std::pair<int, int> pivot = quickFlag(arr, left, right);
    quickSort(arr, left, pivot.first);
    quickSort(arr, pivot.second, right);
}
/*
*快排递归主函数
*/
void quickSort(int arr[], int n)
{
    quickSort(arr, 0, n-1);
}

非递归实现(栈):

void quickSortStack(int arr[], int n)
{
    std::stack<std::pair<int, int>> stk;
    std::pair<int, int> pivot(0, n-1);
    stk.push(pivot);
    // 循环条件,栈不为空
    while(!stk.empty())
    {
        pivot = stk.top();
        stk.pop();
        std::pair<int, int> newPivot = quickFlag(arr, pivot.first, pivot.second);
        if(pivot.first < newPivot.first)
        {
            stk.push(std::make_pair(pivot.first, newPivot.first));
        }
        if(newPivot.second < pivot.second)
        {
            stk.push(std::make_pair(newPivot.second, pivot.second));
        }
    }
}
时间复杂度空间复杂度稳定性
最好平均最坏辅助空间每次排序后,是否会改变相同数的次序。
O(N*logN)O(N*logN)O(N^2)O(logN)~O(N)不稳定

本篇全部主调函数:

#include "SortReview.h"  // 作者创建的头文件

int main()
{
	SortReview s;
	int arr[10];
	int n = sizeof(arr) / sizeof(arr[0]);

	std::cout << "Bubble Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.bubbleSort(arr, n);
	s.printArr(arr, n);

	std::cout << "Bubble Plus Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.bubbleSortPlus(arr, n);
	s.printArr(arr, n);

	std::cout << "Insert Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.insertSort(arr, n);
	s.printArr(arr, n);

	std::cout << "Shell Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.shellSort(arr, n);
	s.printArr(arr, n);

	std::cout << "Select Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.selectSort(arr, n);
	s.printArr(arr, n);
	
	std::cout << "Heap Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.heapSort(arr, n);
	s.printArr(arr, n);

	std::cout << "Bucket Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.bucketSort(arr, n);
	s.printArr(arr, n);

	std::cout << "Merge Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.mergeSort(arr, n);
	s.printArr(arr, n);

	std::cout << "MergeWhile Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.mergeSortWhile(arr, n);
	s.printArr(arr, n);

	std::cout << "Quick Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.quickSort(arr, n);
	s.printArr(arr, n);

	std::cout << "Quick Stack Sort : " << std::endl;
	s.randomArr(arr, n);
	s.printArr(arr, n);
	s.quickSortStack(arr, n);
	s.printArr(arr, n);
}

九、排序总结:

分类:
简单算法:冒泡、直接插排、简单选排、桶排
改进算法:shell(希尔排序——直接插排plus)
堆排(选择排序plus)
归并排序
快排(冒泡排序plus)

交换排序类插入排序类选择排序类归并类基数类
冒泡 快排插排 shell选排 堆排归并桶排

注:标红的是笔者认为比较重要的,当然这只是一个相对性。

学习笔记:

  1. 数据量少或基本有序时,不考虑复杂的改进算法;
  2. 对内存要求比较高时,归并和排序不是好选择;
  3. 如果内存无要求,归并绝对是强者;

注:当然,他们的优劣势还有很多,读者可以根据需要总结并应用。

2020/06/28 13:46
@luxurylu
点个赞鼓励下笔者~
如有错误欢迎指出,感谢~

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值