活动地址:CSDN21天学习挑战赛
目录
为了避免自己想数据就整了一个函数专门实现生成打乱前的数据的函数
算法简介
- At first,我们先look一张我从某文章*的表格,来了解一下这几大算法
- 图都看了,那我也不得不提一嘴,图里面的东西了
- 算法效率
- 在一个算法设计完成后,还需要对算法的执行情况做一个评估。一个好的算法,可以大幅度的节省运行的资源消耗和时间。在进行评估时不需要太具体,毕竟数据量是不确定的,通常是以数据量为基准来确定一个量级,通常会使用到时间复杂度和空间复杂度这两个概念。
- 时间复杂度
-
通常把算法中的基本操作重复执行的频度称为算法的时间复杂度。算法中的基本操作一般是指算法中最深层循环内的语句(赋值、判断、四则运算等基础操作)。我们可以把时问频度记为T(n),它与算法中语句的执行次数成正比。其中的n被称为问题的规模,大多数情况下为输入的数据量。
-
-
空间复杂度
-
程序从开始执行到结束所需要的内存容量,也就是整个过程中最大需要占用多少的空间。为了评估算法本身,输入数据所占用的空间不会考虑,通常更关注算法运行时需要额外定义多少临时变量或多少存储结构。如:如果需要借助一个临时变量来进行两个元素的交换,则空间复杂度为O(1).
-
- 算法效率
时间复杂度与空间复杂度
时间复杂度
- 最坏的情况
- 最坏的情况莫过于,完整的遍历了整个集合,也没有找到需要找的的元素,循环执行次数与n相关,所以时间复杂度为O(n)
- 最好的情况
- 第一次循环便找到了元素,则时间复杂度为常数级O(1);
- 平常的情况
- 综合两种情况,顺序查找的时间复杂度为O(n),属于查找较慢的算法。
空间复杂度
算法不会改变原有的元素集合,只需要一个额外的变量控制索引变化,所以空间复杂度为常数级:O(1)。
一、冒泡排序
- 冒泡排序,作为最基本的排序算法,由于原理像冒泡一样,所以取名为冒泡排序;我们知道,水泡在上升时,总是密度最小的最先上去,假如一个水层只能容纳一个水泡,那么水泡由上到下的排序就是密度逐渐增大的排序。类似的,我们可以实现一个相似的排序算法,冒泡排序。
下面给出冒泡排序的过程图
冒泡排序,可以从左往右,也可以从右往左,全看心情,比较两个相邻的元素。比大比小,也是全部看心情。
代码与思路
算法思路
- 比较相邻的两个元素,如过前面的元素比后面的大,就将它们两个交换位置。
- 对所有元素做完上面的操作,从第一对再到最后一对,就这样,最后的元素是最大的数
- 对于所有的元素再进行第一步的操作,出去最后一个
算法代码
//c++代码
void BubbleSort_orderly(vector<int>& v) {
int len = v.size();
bool orderly = false;
for (int i = 0; i < len - 1 && !orderly; ++i) {
orderly = true;
for (int j = 0; j < len - 1 - i; ++j) {
if (v[j] > v[j + 1]) { // 从小到大
orderly = false; // 发生交换则仍非有序
swap(v[j], v[j + 1]);
}
}
}
}
//全部源代码
#include <vector>
#include <iostream>
using namespace std;
vector <int> v{ 2,3,1,4,5,6,7,8,10,23,20 };
void BubbleSorderly(vector<int>& v) {
//打印交换前的v向量
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++){
cout << v.at(i) << " ";
}
int len = v.size();
bool orderly = false;
for (int i = 0; i < len - 1 && !orderly; ++i) {
orderly = true;
for (int j = 0; j < len - 1 - i; ++j) {
if (v[j] > v[j + 1]) { // 从小到大
orderly = false; // 发生交换则仍非有序
swap(v[j], v[j + 1]);
}
}
}
cout << "排序后" << endl;
//打印整个vector
for (int i = 0; i <= v.size() - 1; i++){
cout << v.at(i) <<" ";
}
}
int main()
{
BubbleSorderly(v);
return 0;
}
这么简单的源代码就不讲了,你们不会要求我讲吧?
# coding=utf-8
def bubble_sort(array):
for i in range(1, len(array)):
for j in range(0, len(array)-i):
if array[j] > array[j+1]:
array[j], array[j+1] = array[j+1], array[j]
return array
if __name__ == '__main__':
array = [10, 17, 50, 7, 30, 24, 27, 45, 15, 5, 36, 21]
print(bubble_sort(array))
package day0515;
public class demo_sort {
public static void main(String[] args) {
//冒泡排序算法
int[] numbers=new int[]{1,5,8,2,3,9,4};
//需进行length-1次冒泡
for(int i=0;i<numbers.length-1;i++)
{
for(int j=0;j<numbers.length-1-i;j++)
{
if(numbers[j]>numbers[j+1])
{
int temp=numbers[j];
numbers[j]=numbers[j+1];
numbers[j+1]=temp;
}
}
}
System.out.println("从小到大排序后的结果是:");
for(int i=0;i<numbers.length;i++)
System.out.print(numbers[i]+" ");
}
}
#region 冒泡排序
//针对数组的排序
//每一轮让第一个数与下一个比,比到没有比它大的小的
int[] arr_1 = new int[10];
bool isShot;
Console.WriteLine("输入十个数字");
//遍历输入数组
for (int i = 0; i <arr_1.Length; i++)
{
arr_1[i] = int.Parse(Console.ReadLine());
}
//排序
for (int m = 0; m <arr_1.Length; m++)
{
isShot = false;
//外循环
for (int i = 0; i < (arr_1.Length-1-m); i++)
{
//内循环
if (arr_1[i]>arr_1[i+1])
{
isShot = true;
int temp = arr_1[i];
arr_1[i] = arr_1[i+1];
arr_1[i + 1] = temp;
}
}
if (!isShot)
{
break;
}
}
//遍历打印
for (int i = 0; i < arr_1.Length; i++)
{
Console.Write(arr_1[i]+" ");
}
//优化方法
//1.比完后确定了位置的数不用参与比较了
//内循环里再减m
//2.特殊情况的优化
//如果第一轮就已经排好了,就直接跳出
//加一个bool型变量进行判断
二、选择排序
选择排序(Selection sort)是一种简单直观的排序算法。
先上图(无图无真相)
工作原理
- 第一次从待排序数据中选择出最小或者最大的数据,然后存放的数据的起始位置。
- 然后再从未排序数据中,找出最小或者最大的元素,然后放到已排序数据为末尾。
- 以此类推,知道虚排序的数据个数为0。才算排序结束
代码与实现
void SelectionSort(vector<int>& v) {
int min, len = v.size();
for (int i = 0; i < len - 1; ++i) {
min = i;
for (int j = i + 1; j < len; ++j) {
if (v[j] < v[min]) { // 标记最小的
min = j;
}
}
if (i != min) // 交换到前面
swap(v[i], v[min]);
}
}
//选择排序
void SelectionSort(vector<int>& v) {
int min, len = v.size();
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
for (int i = 0; i < len - 1; ++i) {
min = i;
for (int j = i + 1; j < len; ++j) {
if (v[j] < v[min]) { // 标记最小的
min = j;
}
}
if (i != min) // 交换到前面
swap(v[i], v[min]);
}
cout << endl << "排序后" << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
}
为了避免自己想数据就整了一个函数专门实现生成打乱前的数据的函数
void randperm(int Num)
{
srand((int)time(0));
vector<int> temp;
for (int i = 0; i < Num; ++i)
{
temp.push_back(i + 1);
}
random_shuffle(temp.begin(), temp.end());
for (int i = 0; i < temp.size(); i++)
{
cout << temp[i] << ",";
}
getchar();
}
#include <vector>
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <algorithm>
using namespace std;
vector <int> y{ 39,29,30,22,7,43,1,5,48,27,11,25,4,
24,40,50,18,37,19,42,9,45,2,12,47,32,36,21,3,13,6,35,
28,17,41,14,8,10,16,46,15,26,44,33,49,31,20,34,23,38};
//生成打乱的数据
void randperm(int Num)
{
srand((int)time(0));
vector<int> temp;
for (int i = 0; i < Num; ++i)
{
temp.push_back(i + 1);
}
random_shuffle(temp.begin(), temp.end());
for (int i = 0; i < temp.size(); i++)
{
cout << temp[i] << ",";
}
getchar();
}
//选择排序
void SelectionSort(vector<int>& v) {
int min, len = v.size();
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
for (int i = 0; i < len - 1; ++i) {
min = i;
for (int j = i + 1; j < len; ++j) {
if (v[j] < v[min]) { // 标记最小的
min = j;
}
}
if (i != min) // 交换到前面
swap(v[i], v[min]);
}
cout << endl << "排序后" << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
}
//冒泡排序
void BubbleSorderly(vector<int>& v)
{
//打印交换前的v向量
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
int len = v.size();
bool orderly = false;
for (int i = 0; i < len - 1 && !orderly; ++i) {
orderly = true;
for (int j = 0; j < len - 1 - i; ++j) {
if (v[j] > v[j + 1]) { // 从小到大
orderly = false; // 发生交换则仍非有序
swap(v[j], v[j + 1]);
}
}
}
cout << endl << "排序后" << endl;
//打印整个vector
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
getchar();
}
int main()
{
SelectionSort(y);
getchar();
return 0;
}
三、插入排序
- 最差时间复杂度:O(n^2)
- 最优时间复杂度:O(n)
- 平均时间复杂度:O(n^2)
- 稳定性:稳定
这个插入排序有点像打扑克。
- 该算法的思想就和我们打扑克牌的时候一样,从牌堆里摸出来的牌顺序都是乱的,这是我们就要给扑克牌排序,
- 按照习惯,我们摸牌的时候,会把牌自动放到左边的牌一直保持有序的位置,让手中的牌一直保持有序的状态,
具体如下图所示:
算法具体思路(抽象):
1. 从第一个元素开始,该元素可以认为已经被排序
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5. 将新元素插入到该位置后
6. 重复步骤2~5
源码
void InsertSort(vector<int>& v)
{
int len = v.size();
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
for (int i = 1; i < len; ++i) {
int temp = v[i];
for (int j = i - 1; j >= 0; --j)
{
if (v[j] > temp)
{
v[j + 1] = v[j];
v[j] = temp;
}
else
break;
}
}
cout << endl << "排序后" << endl;
//打印整个vector
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
}
#include <vector>
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <algorithm>
using namespace std;
void BubbleSorderly(vector<int>& v);
void SelectionSort(vector<int>& v);
vector <int> v{ 13,15,2,6,8,7,3,10,12,5,9,1,11,14,4};
//生成打乱的数据
void randperm(int Num)
{
srand((int)time(0));
vector<int> temp;
for (int i = 0; i < Num; ++i)
{
temp.push_back(i + 1);
}
random_shuffle(temp.begin(), temp.end());
for (int i = 0; i < temp.size(); i++)
{
cout << temp[i] << ",";
}
getchar();
}
void InsertSort(vector<int>& v)
{
int len = v.size();
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
for (int i = 1; i < len; ++i) {
int temp = v[i];
for (int j = i - 1; j >= 0; --j)
{
if (v[j] > temp)
{
v[j + 1] = v[j];
v[j] = temp;
}
else
break;
}
}
cout << endl << "排序后" << endl;
//打印整个vector
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
}
int main()
{
//randperm(15);
InsertSort(v);
getchar();
return 0;
}
//选择排序
void SelectionSort(vector<int>& v) {
int min, len = v.size();
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
for (int i = 0; i < len - 1; ++i) {
min = i;
for (int j = i + 1; j < len; ++j) {
if (v[j] < v[min]) { // 标记最小的
min = j;
}
}
if (i != min) // 交换到前面
swap(v[i], v[min]);
}
cout << endl << "排序后" << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
}
//冒泡排序
void BubbleSorderly(vector<int>& v)
{
//打印交换前的v向量
cout << "排序前 " << endl;
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
int len = v.size();
bool orderly = false;
for (int i = 0; i < len - 1 && !orderly; ++i) {
orderly = true;
for (int j = 0; j < len - 1 - i; ++j) {
if (v[j] > v[j + 1]) { // 从小到大
orderly = false; // 发生交换则仍非有序
swap(v[j], v[j + 1]);
}
}
}
cout << endl << "排序后" << endl;
//打印整个vector
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
getchar();
}
算法测试
- 输入数据 vector <int> v{ 13,15,2,6,8,7,3,10,12,5,9,1,11,14,4};
- 结果
四、希尔排序
关于希尔排序
希尔排序这个名字,来源于它的发明者希尔,也称作“缩小增量排序”,是插入排序的一种更高效的改进版本。
两个49在排序前后位置颠倒了,所以希尔排序是不稳定的
- 我们知道,插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。
实行过程图解
void print(int a[], int n,int cases, vector<int>& v)
{
switch (cases)
{
case 1:
for (int j = 0; j < n; j++)
{
cout << a[j] << " ";
}
cout << endl;
break;
case 2:
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
break;
default:
printf("cases error");
break;
}
}
void shellSort(int a[], int n) //a -- 待排序的数组, n -- 数组的长度
{
int i, j, gap; // gap为步长,每次减为原来的一半。
for (gap = n / 2; gap > 0; gap /= 2)
{
// 共gap个组,对每一组都执行直接插入排序
for (i = 0; i < gap; i++)
{
for (j = i + gap; j < n; j += gap)
{
// 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。
if (a[j] < a[j - gap])
{
int tmp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > tmp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = tmp;
}
}
}
}
}
#include <vector>
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <algorithm>
using namespace std;
vector <int> v{2,223,34,4,4,45,4,66,5768,9,9,9};
int a[20] = { 6,10,14,3,16,11,12,15,5,20,9,13,7,18,17,1,8,2,19,4 };
//生成打乱的数据
void randperm(int Num)
{
srand((int)time(0));
vector<int> temp;
for (int i = 0; i < Num; ++i)
{
temp.push_back(i + 1);
}
random_shuffle(temp.begin(), temp.end());
for (int i = 0; i < temp.size(); i++)
{
cout << temp[i] << ",";
}
getchar();
}
void print(int a[], int n,int cases, vector<int>& v)
{
switch (cases)
{
case 1:
for (int j = 0; j < n; j++)
{
cout << a[j] << " ";
}
cout << endl;
break;
case 2:
for (int i = 0; i <= v.size() - 1; i++) {
cout << v.at(i) << " ";
}
break;
default:
printf("cases error");
break;
}
}
void shellSort(int a[], int n) //a -- 待排序的数组, n -- 数组的长度
{
int i, j, gap; // gap为步长,每次减为原来的一半。
for (gap = n / 2; gap > 0; gap /= 2)
{
// 共gap个组,对每一组都执行直接插入排序
for (i = 0; i < gap; i++)
{
for (j = i + gap; j < n; j += gap)
{
// 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。
if (a[j] < a[j - gap])
{
int tmp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > tmp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = tmp;
}
}
}
}
}
int main()
{
//randperm(20);
//print(a,20,1,v);
cout << "初始序列:"<< endl;
print(a, 20,1,v);
shellSort(a, 20);
cout << "排序结果:"<< endl;
print(a, 20,1, v);
getchar();
return 0;
}
算法测试
五、归并排序
- 什么是归并排序
- 归并字面上的意思是合并,归并算法的核心思想是分治法,就是将一个数组一刀切两半,递归切,直到切成单个元素,然后重新组装合并,单个元素合并成小数组,两个小数组合并成大数组,直到最终合并完成,排序完毕。
看图说话
伪代码
n1=q-p+1;
n2=r-q;
create new arrays L[n1+1] and R[n2+1]
for i=0 to n1-1
L[i]=A[p+i]
for j=0 to n2-1
R[j]=A[q+1+j]
L[n1]=1000//假设1000是无穷大
R[n2]=1000
i=j=0
for k=p to r
if(L[i]<=R[j])
A[k]=L[i]
i=i+1
else
A[k]=R[j]
j=j+1
MERGE_SORT(A,p,r)
if p<r
q=floor((p+r)/2)
MERGE_SORT(A,p,q)
MERGE_SORT(A,q+1,r)
MERGE(A,p,q,r)
~~~
代码实现
template<typename T>
void merge_sort(T arr[], int len) {
T* a = arr;
T* b = new T[len];
for (int seg = 1; seg < len; seg += seg) {
for (int start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
T* temp = a;
a = b;
b = temp;
}
if (a != arr) {
for (int i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
delete[] b;
}
template<typename T>
void merge_sort_recursive(T arr[], T reg[], int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
import java.util.Arrays;
//归并排序:先分再合
public class MergeSort {
public static void main(String[] args) {
int[] arr = {3,1,4,6,22,0,33,2,745,5,56,8};
int[] temp = new int[arr.length];
System.out.println("未排序数组:"+ Arrays.toString(arr));
mergeSort(arr,0,arr.length-1,temp);
System.out.println("已排序数组:"+ Arrays.toString(arr));
}
//mergeSort()方法:将数组分组出来。
public static void mergeSort(int[] arr,int left,int right,int[] temp){
//将数组进行分组
if (left < right){
int l = left;
int r = right;
int middle = (l+r)/2;
//将分组进行排序整合
merge(arr,left,middle,right,temp);//将左边的部分继续分
mergeSort(arr,0,middle,temp);//将右边的部分继续分
mergeSort(arr,middle+1,r,temp);
}
}
public static void merge(int[] arr,int left,int middle,int right,int[] temp){
int l = left;
int r = middle+1;
int t = 0;//用于临时数组下标索引
while(l <= middle && r <= right){
//先将两个部分整合
temp[t++] = arr[l] <= arr[r]?arr[l++] : arr[r++];
// if (arr[l] <= arr[r]){
// temp[t] = arr[l];
// t++;l++;
// }else{
// temp[t] = arr[r];
// t++;r++;
// }
}
//如果左边的部分还有元素没有被合并,则接着l继续合并
while(l <= middle){
temp[t++] = arr[l++];
// temp[t] = arr[l];
// t++;l++;
}
//如果右边的部分还有元素没有被合并,则接着r继续合并
while (r <= right){
temp[t++] = arr[r++];
// temp[t] = arr[r];
// t++;r++;
}
//将temp临时数组中的元素顺序传到arr数组中
t = 0;
int tempLeft = left;
while(tempLeft <= right){
arr[tempLeft] = temp[t];
tempLeft++;t++;
}
}
}
六、快速排序
- 快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。
上图!(借的)
活动地址:CSDN21天学习挑战赛
正文
-
At first,我们先look一张我从某文章*的表格,来了解一下这几大算法
-
图都看了,那我也不得不提一嘴,图里面的东西了
-
算法效率
在一个算法设计完成后,还需要对算法的执行情况做一个评估。一个好的算法,可以大幅度的节省运行的资源消耗和时间。在进行评估时不需要太具体,毕竟数据量是不确定的,通常是以数据量为基准来确定一个量级,通常会使用到时间复杂度和空间复杂度这两个概念。 -
时间复杂度
通常把算法中的基本操作重复执行的频度称为算法的时间复杂度。算法中的基本操作一般是指算法中最深层循环内的语句(赋值、判断、四则运算等基础操作)。我们可以把时问频度记为T(n),它与算法中语句的执行次数成正比。其中的n被称为问题的规模,大多数情况下为输入的数据量。对于每一段代码,都可以转化为常数或与n相关的函数表达式,记做f(n)。如果我们把每一段代码的花费的时间加起来就能够得到一个刻画时间复杂度的表达式,在合并后保留量级最大的部分即可确定时间复杂度,记做O(f(n)),其中的O就是代表数量级。
常见的时间复杂度有(由低到高): O(1)、O(log2 n)、O(n)、O(n log2n)、O(n2)、o(n3)、O(2")、O(nl)。 -
空间复杂度
程序从开始执行到结束所需要的内存容量,也就是整个过程中最大需要占用多少的空间。为了评估算法本身,输入数据所占用的空间不会考虑,通常更关注算法运行时需要额外定义多少临时变量或多少存储结构。如:如果需要借助一个临时变量来进行两个元素的交换,则空间复杂度为O(1).
-
时间复杂度与空间复杂度的分类
时间复杂度
-
最坏的情况
最坏的情况莫过于,完整的遍历了整个集合,也没有找到需要找的的元素,循环执行次数与n相关,所以时间复杂度为O(n). -
最好的情况
第一次循环便找到了元素,则时间复杂度为常数级O(1); -
平常的情况
综合两种情况,顺序查找的时间复杂度为O(n),属于查找较慢的算法。
空间复杂度
算法不会改变原有的元素集合,只需要一个额外的变量控制索引变化,所以空间复杂度为常数级:O(1)。
快速排序
- 快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。
上图(借的):
(小数,基准元素,大数)。在区间中随机挑选一个元素作基准,将小于基准的元素放在基准之前,大于基准的元素放在基准之后,再分别对小数区与大数区进行排序。
快速排序思路:
- 选取第一个数为基准
- 将比基准小的数交换到前面,比基准大的数交换到后面
- 对左右区间重复第二步,直到各区间只有一个数
快速排序的时间复杂度和归并排序一样,O(n log n),但这是建立在每次切分都能把数组一刀切两半差不多大的前提下,如果出现极端情况,比如排一个有序的序列,如[ 9,8,7,6,5,4,3,2,1 ],选取基准值 9 ,那么需要切分 n – 1 次才能完成整个快速排序的过程,这种情况下,时间复杂度就退化成了 O(n^2),当然极端情况出现的概率也是比较低的。
所以说,快速排序的时间复杂度是 O(nlogn),极端情况下会退化成 O(n^2),为了避免极端情况的发生,选取基准值应该做到随机选取,或者是打乱一下数组再选取。
另外,快速排序的空间复杂度为 O(1)。
讲解
我们对一下是个数进行快速排序操作:
- 首先在这个序列中随便选择一个数作为基准数,这里我们为了方便就选1,作为基准数。
-
在初始状态下,数字1在序列的第1位。我们的目标是将1挪到序列中间的某个位置,假设这个位置是 k 。现在就需要寻找这个 k ,并且以第k位为分界点,k左边的数都 ≤ 1,k,右边的数都 ≥ 1。那么如何找到这个位置 k 呢?
-
我们知道,快速排序其实是冒泡排序的一种改进,冒泡排序每次对相邻的两个数进行比较,这显然是一种比较浪费时间的。会大大提高循环次数。
-
而快速排序是分别从两端开始”检测”的,先从右往左找一个小于1的数,再从左往右找一个大于1的数,然后交换他们。这里可以用两个变量 i 和 j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“门神i”和“门神j”。刚开始的时候让门神1指向序列的最左边,指向数字1。让门神j指向序列的最右边,指向数字2
代码
void QuickSort(vector<int>& v, int low, int high) {
if (low >= high) // 结束标志
return;
int first = low; // 低位下标
int last = high; // 高位下标
int key = v[first]; // 设第一个为基准
while (first < last)
{
// 将比第一个小的移到前面
while (first < last && v[last] >= key)
last--;
if (first < last)
v[first++] = v[last];
// 将比第一个大的移到后面
while (first < last && v[first] <= key)
first++;
if (first < last)
v[last--] = v[first];
}
// 基准置位
v[first] = key;
// 前半递归
QuickSort(v, low, first - 1);
// 后半递归
QuickSort(v, first + 1, high);
}
public class QuickSort {
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
//temp就是基准位
temp = arr[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void main(String[] args){
int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
quickSort(arr, 0, arr.length-1);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
伪代码
parttitioned (input list[], input left, input right)
pivot <- list[left]
i <- left
j <- right
while (i < j) do
whlie (i < j and list[j] >= pivot) do
j <- j - 1
end
if (i < j)
list[i] <- list [j]
whlie (i < j and list[i] < pivot) do
i <- i + 1
end
if (i < j)
list[j] <- list [i]
end
list[i] <- pivot
return i
quicksort (input list[], input left, input right)
if (left < right)
mid = parttitioned(list, left, left)
quicksort (list, left, mid-1)
quicksort (list, mid+1, right)
end if
七、堆排序
-
堆排序顾名思义,是利用堆这种数据结构来进行排序的算法。堆是一种优先队列,两种实现,最大堆和最小堆,由于我们这里排序按升序排,所以就直接以最大堆来说吧。
-
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
-
我们完全可以把堆(以下全都默认为最大堆)看成一棵完全二叉树,但是位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,由于堆要时刻保持这样的规则特性,所以一旦堆里面的数据发生变化,我们必须对堆重新进行一次构建。
既然堆顶元素永远都是整棵树中的最大值,那么我们将数据构建成堆后,只需要从堆顶取元素不就好了吗? 第一次取的元素,是否取的就是最大值?取完后把堆重新构建一下,然后再取堆顶的元素,是否取的就是第二大的值? 反复的取,取出来的数据也就是有序的数据。
堆排序的实现
#include <iostream>
#include <algorithm>
using namespace std;
// 堆排序:(最大堆,有序区)。从堆顶把根卸出来放在有序区之前,再恢复堆。
void max_heapify(int arr[], int start, int end) {
int dad = start;
int son = dad * 2 + 1;
while (son <= end) {
if (son + 1 <= end && arr[son] < arr[son + 1])
son++;
if (arr[dad] > arr[son])
return;
else {
swap(arr[dad], arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
for (int i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
for (int i = len - 1; i > 0; i--) {
swap(arr[0], arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main() {
int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
return 0;
}
八、计数排序
思路
- 该算法的时间复杂了:O(n+k)。
- 该算法的空间复杂度:O(k)
- 稳定性: 稳定
计数排序是一种非基于比较的排序算法,我们之前介绍的各种排序算法几乎都是基于元素之间的比较来进行排序的,计数排序的时间复杂度为 O(n + m ),m 指的是数据量,说的简单点,计数排序算法的时间复杂度约等于 O(n),快于任何比较型的排序算法。
- 计数排序:统计小于等于该元素值的元素的个数i,于是该元素就放在目标数组的索引i位(i≥0)。
- 计数排序基于一个假设,待排序数列的所有数均为整数,且出现在(0,k)的区间之内。
- 如果 k(待排数组的最大值) 过大则会引起较大的空间复杂度,一般是用来排序 0 到 100 之间的数字的最好的算法,但是它不适合按字母顺序排序人名。
- 计数排序不是比较排序,排序的速度快于任何比较排序算法。 时间复杂度为 O(n+k),空间复杂度为 O(n+k)
算法的步骤如下:
1. 找出待排序的数组中最大和最小的元素
2. 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项
3. 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加)
4. 反向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项,每放一个元素就将 C[i] 减去 1
代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 计数排序
void CountSort(vector<int>& vecRaw, vector<int>& vecObj)
{
// 确保待排序容器非空
if (vecRaw.size() == 0)
return;
// 使用 vecRaw 的最大值 + 1 作为计数容器 countVec 的大小
int vecCountLength = (*max_element(begin(vecRaw), end(vecRaw))) + 1;
vector<int> vecCount(vecCountLength, 0);
// 统计每个键值出现的次数
for (int i = 0; i < vecRaw.size(); i++)
vecCount[vecRaw[i]]++;
// 后面的键值出现的位置为前面所有键值出现的次数之和
for (int i = 1; i < vecCountLength; i++)
vecCount[i] += vecCount[i - 1];
// 将键值放到目标位置
for (int i = vecRaw.size(); i > 0; i--) // 此处逆序是为了保持相同键值的稳定性
vecObj[--vecCount[vecRaw[i - 1]]] = vecRaw[i - 1];
}
int main()
{
vector<int> vecRaw = { 0,5,7,9,6,3,4,5,2,8,6,9,2,1 };
vector<int> vecObj(vecRaw.size(), 0);
CountSort(vecRaw, vecObj);
for (int i = 0; i < vecObj.size(); ++i)
cout << vecObj[i] << " ";
cout << endl;
return 0;
}
public class CountingSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxValue = getMaxValue(arr);
return countingSort(arr, maxValue);
}
private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
}
def countingSort(arr, maxValue):
bucketLen = maxValue+1
bucket = [0]*bucketLen
sortedIndex =0
arrLen = len(arr)
for i in range(arrLen):
if not bucket[arr[i]]:
bucket[arr[i]]=0
bucket[arr[i]]+=1
for j in range(bucketLen):
while bucket[j]>0:
arr[sortedIndex] = j
sortedIndex+=1
bucket[j]-=1
return arr
局限性
计数排序的毛病很多,我们来找找 bug 。
如果我要排的数据里有 0 呢? int[] 初始化内容全是 0 ,排毛线。
如果我要排的数据范围比较大呢?比如[ 1,9999 ],我排两个数你要创建一个 int[10000] 的数组来计数?
对于第一个 bug ,我们可以使用偏移量来解决,比如我要排[ -1,0,-3 ]这组数字,这个简单,我全给你们加 10 来计数,变成[ 9,10,7 ]计完数后写回原数组时再减 10。不过有可能也会踩到坑,万一你数组里恰好有一个 -10,你加上 10 后又变 0 了,排毛线。
对于第二个 bug ,确实解决不了,如果是[ 9998,9999 ]这种虽然值大但是相差范围不大的数据我们也可以使用偏移量解决,比如这两个数据,我减掉 9997 后只需要申请一个 int[3] 的数组就可以进行计数。
1.当数列最大最小值差距过大时,并不适用于计数排序
比如给定 20 个随机整数,范围在 0 到 1 亿之间,此时如果使用计数排序的话,就需要创建长度为 1 亿的数组,不但严重浪费了空间,而且时间复杂度也随之升高。
2.当数列元素不是整数时,并不适用于计数排序
如果数列中的元素都是小数,比如 3.1415,或是 0.00000001 这样子,则无法创建对应的统计数组,这样显然无法进行计数排序。
正是由于这两大局限性,才使得计数排序不像快速排序、归并排序那样被人们广泛适用。
由此可见,计数排序只适用于正整数并且取值范围相差不大的数组排序使用,它的排序的速度是非常可观的。
九、桶排序
桶排序思路
桶排序一般应用在服务器的文件查找中,因为服务器一般不能把所有数据都加载到内存当中,这时候就可以利用桶排序进行解决了。
- 桶排序,是一种基于非比较的排序方式,时间复杂度O(n),因此它是是属于一种“线性排序”。
- 思想:桶排序的思想是将一组数据分到几个有序的桶里,每个桶里的数据再单独进行快速排序。每个桶内都排序完成后,再加上本身桶之间已经是有序的,那么按照桶的顺序依次取出每个桶内的数据,最终组成的序列就是有序的。
- 桶排序具体思路
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 对每个不是空的桶子进行排序。
假设数据分布在[0,100)之间,每个桶内部用链表表示,在数据入桶的同时插入排序,然后把各个桶中的数据合并。
稳定性
根据桶排序的排序原理,会将待排序列表进行分桶、桶内排序和合并。在对每一个桶进行桶内排序时,可以采用不同的排序算法,有些排序算法是稳定的,有些排序算法是不稳定,这会影响到桶排序的稳定性。所以桶排序的稳定性取决于桶内排序算法的稳定性。
时间复杂度
- 在桶排序中,需要走访待排序列表中的每一个元素,进行分桶,列表长度为 n ,然后需要对每一个桶进行桶内排序,单个桶内排序的最坏时间复杂度是 O(ni^2),ni 表示第 i 个桶内有 ni 个数据,一共有 k 个桶,时间复杂度为n加每一个桶内排序的时间复杂度,最坏情况下所有数据全被分到了一个桶内,ni=n,时间复杂度为T(n)=n+n^2,再乘分桶和排序的步骤数(常数,不影响大O记法),所以桶排序的时间复杂度为 O(n^2) 。
- 桶排序的最优情况是将数据均匀地分配到每一个桶中,此时有k个桶,每个桶内有n/k个数据,每个桶内排序的平均时间复杂度为O(n/klogn/k),整个桶排序的时间复杂度为T(n)=n+kn/k*logn/k,而当k=n时,即每个桶内只有一个元素(不需要进行桶内排序),时间复杂度为O(n)
代码
public class BucketSort {
/**
* 桶排序
* @param args
*/
public static void main(String[] args) {
int[] arr = {10, 0, 80, 40, 90, 22, 60, 30, 55, 77, 80, 50};
buckSort(arr);
for (int i : arr) {
System.out.print(i + " ");
}
}
private static void buckSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int min = arr[0];
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (min > arr[i]) {
min = arr[i];
}
if (max < arr[i]) {
max = arr[i];
}
}
// 桶数量,桶的范围是arr.length,注意桶的范围是自定义的
int buckNum = (max - min) / arr.length + 1;
List<List<Integer>> list = new ArrayList<>(buckNum);
for (int i = 0; i < buckNum; i++) {
list.add(new ArrayList<>());
}
// 将元素放入桶中
for (int i = 0; i < arr.length; i++) {
int index = (arr[i] - min) / arr.length;
List<Integer> integers = list.get(index);
integers.add(arr[i]);
}
for (List<Integer> integers : list) {
if (integers == null || integers.size() == 0) {
continue;
}
// 使用归并排序
Collections.sort(integers);
}
int i = 0;
for (List<Integer> integers : list) {
if (integers == null || integers.size() == 0) {
continue;
}
for (Integer integer : integers) {
arr[i++] = integer;
}
}
}
}
#include<iterator>
#include<iostream>
#include<vector>
using std::vector;
const int BUCKET_NUM = 10;
struct ListNode{
explicit ListNode(int i=0):mData(i),mNext(NULL){}
ListNode* mNext;
int mData;
};
ListNode* insert(ListNode* head,int val){
ListNode dummyNode;
ListNode *newNode = new ListNode(val);
ListNode *pre,*curr;
dummyNode.mNext = head;
pre = &dummyNode;
curr = head;
while(NULL!=curr && curr->mData<=val){
pre = curr;
curr = curr->mNext;
}
newNode->mNext = curr;
pre->mNext = newNode;
return dummyNode.mNext;
}
ListNode* Merge(ListNode *head1,ListNode *head2){
ListNode dummyNode;
ListNode *dummy = &dummyNode;
while(NULL!=head1 && NULL!=head2){
if(head1->mData <= head2->mData){
dummy->mNext = head1;
head1 = head1->mNext;
}else{
dummy->mNext = head2;
head2 = head2->mNext;
}
dummy = dummy->mNext;
}
if(NULL!=head1) dummy->mNext = head1;
if(NULL!=head2) dummy->mNext = head2;
return dummyNode.mNext;
}
void BucketSort(int n,int arr[]){
vector<ListNode*> buckets(BUCKET_NUM,(ListNode*)(0));
for(int i=0;i<n;++i){
int index = arr[i]/BUCKET_NUM;
ListNode *head = buckets.at(index);
buckets.at(index) = insert(head,arr[i]);
}
ListNode *head = buckets.at(0);
for(int i=1;i<BUCKET_NUM;++i){
head = Merge(head,buckets.at(i));
}
for(int i=0;i<n;++i){
arr[i] = head->mData;
head = head->mNext;
}
}
# coding=utf-8
def bucket_sort(array):
min_num, max_num = min(array), max(array)
bucket_num = (max_num-min_num)//3 + 1
buckets = [[] for _ in range(int(bucket_num))]
for num in array:
buckets[int((num-min_num)//3)].append(num)
new_array = list()
for i in buckets:
for j in sorted(i):
new_array.append(j)
return new_array
if __name__ == '__main__':
array = [5, 7, 3, 7, 2, 3, 2, 5, 9, 5, 7, 8]
print(bucket_sort(array))
十、基数排序
-
基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将数据按位数切割成不同的数字,然后按每个位数分别比较。它是一种非比较型的排序算法,最早用于解决卡片排序的问题。
-
它的工作原理是将待排序的元素拆分为k个关键字,其中k为最大值的位数,从低位开始进行稳定排序。(注意:数列中的元素都是非负整数)
-
基数排序是一种稳定的排序算法
-
假设说,我们要对 100 万个手机号码进行排序,应该选择什么排序算法呢?排的快的有归并、快排时间复杂度是 O(nlogn),计数排序和桶排序虽然更快一些,但是手机号码位数是11位,那得需要多少桶?内存条表示不服。
这个时候,我们使用基数排序是最好的选择。
代码与实现
// 基数排序:一种多关键字的排序算法,可用桶排序实现。
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int maxData = data[0]; ///< 最大数
/// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
for (int i = 1; i < n; ++i)
{
if (maxData < data[i])
maxData = data[i];
}
int d = 1;
int p = 10;
while (maxData >= p)
{
//p *= 10; // Maybe overflow
maxData /= 10;
++d;
}
return d;
/* int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;*/
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = new int[n];
int *count = new int[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete []tmp;
delete []count;
}
import java.util.ArrayList;
import java.util.Arrays;
//基数排序
public class RadixSort {
public static void main(String[] args) {
int[] array = new int[]{20, 21, 8, 30, 20, 6, 51, 14};
RadixSort(array);
System.out.println(Arrays.toString(array));
}
public static int[] RadixSort(int[] arr){
if(arr.length == 0 || arr.length ==1)
return arr;
// max 指数组中最大的数,maxDigit 指这个最大的数是几位数
int max = arr[0];
for(int x:arr)
max = Math.max(x,max);
int maxDigit = 0;
while(max != 0){
max /= 10;
maxDigit++;
}
// mod 用于为数组中的数取余数,div 用于把通过 mod 取的余数变成个位数
int mod = 10;
int div = 1;
ArrayList<ArrayList<Integer>> bucket = new ArrayList<ArrayList<Integer>>();
for(int j = 0;j < 10;j++){
bucket.add(new ArrayList<Integer>());
}
for(int i = 0;i<maxDigit;i++,mod *= 10,div *= 10){
// 打印这一轮的排序结果
System.out.println(Arrays.toString(arr));
for(int j = 0;j < arr.length;j++){
// num 指当前元素 arr[j] 的个/十/百/千位是几
int num = (arr[j] % mod) / div;
bucket.get(num).add(arr[j]);
}
int index = 0;
for(int j = 0;j < 10;j++){
if(bucket.get(j).size() != 0){
for(int x:bucket.get(j))
arr[index++] = x;
// 将桶中所有的动态数组清空,否则第二次循环开始再用到这些动态数组时里面还会有数据
bucket.get(j).clear();
}
}
}
return arr;
}
}
def radix_sort(s):
"""基数排序"""
i = 0 # 记录当前正在排拿一位,最低位为1
max_num = max(s) # 最大值
j = len(str(max_num)) # 记录最大值的位数
while i < j:
bucket_list =[[] for _ in range(10)] #初始化桶数组
for x in s:
bucket_list[int(x / (10**i)) % 10].append(x) # 找到位置放入桶数组
print(bucket_list)
s.clear()
for x in bucket_list: # 放回原序列
for y in x:
s.append(y)
i += 1
if __name__ == '__main__':
a = [334,5,67,345,7,345345,99,4,23,78,45,1,3453,23424]
radix_sort(a)
print(a)