文章目录
内存只有 1G,排序100G的数据
1、分割数据:
将100GB的数据分割成100个1GB的块,每个块都可以在内存中排序。
2、排序每个块:
对每个1GB的块进行排序,并将排序后的数据写入单独的文件中。现在你有100个已排序的文件。
3、初始化最小堆:
初始化一个最小堆(或优先队列),并将每个文件的第一个元素插入到这个堆中。
4、多路归并:
每次从最小堆中取出当前最小的元素,并将其写入最终的输出文件中。
更新堆中的元素:从对应的文件中读取下一个元素,并将其重新插入到堆中。
各排序方法时空复杂度
排序的稳定性: 相同的元素,经过排序后,他们的位置谁前谁后还是不变
最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
冒泡 | n | n^2 | 稳定 | ||
选择 | n^2 | n^2 | 不稳定 | ||
插入 | n | n^2 | 稳定 | ||
快排 | nlogn | n^2 | 不稳定 | ||
堆排 | nlogn | nlogn | 不稳定 | ||
希尔 | 不稳定 | ||||
归并 | nlogn | nlogn | 稳定 |
https://www.cnblogs.com/mydesky2012/p/5648087.html
快排
- 快速排序(简称快排):在待排序数组中确定一个基准值(pivot),一次排序后将所有小于基准值的数移动至基准值左侧,大于基准值的数据移动至基准值右侧,这样基准值所在的位置就是最终排序后其应在位置。根据分治、递归的思想,对左右两侧数据递归上面的操作,直至区间缩小为1,所有的数据就都有序了。
双路快排
递归的调用切分方法:首先,随机取数组最左侧元素最为切分元素,数组左端和右端分别有两个指针,从左端开始扫描直到找到一个小于切分值的元素,从右端开始扫描直到找到一个大于切分值的元素,交换两个元素;继续,直到两指针相遇,将左子数组的最右侧元素与切分元素交换;对两个子数组继续调用切分方法,直到整个数组有序。
latest go版本
func sortArray(nums []int) []int {
QuickSort(nums, 0, len(nums)-1)
return nums
}
func QuickSort(list []int, l, r int) {
if l > r {
return
}
l0, r0 := l, r
rand.Seed(time.Now().UnixNano()) // 初始化随机数种子
randomInt := rand.Intn(r-l+1) + l
list[l], list[randomInt] = list[randomInt], list[l]
pivot := list[l]
for l < r {
for l < r && list[r] >= pivot {
r--
}
for l < r && list[l] <= pivot {
l++
}
list[l], list[r] = list[r], list[l]
}
list[l0], list[l] = list[l], list[l0]
QuickSort(list, l0, l-1)
QuickSort(list, l+1, r0)
}
package com.atguigu.com;
import java.util.Arrays;
public class Sort {
//俺的
public void quickSort(int[] nums,int left, int right){
if (left<right){
int privotIndex = getPrivotIndex(nums,left,right);
quickSort(nums,left,privotIndex-1);
quickSort(nums,privotIndex+1,right);
}
}
public int getPrivotIndex(int [] nums, int left, int right){
int pivot = nums[left];
int i = left;
int j = right;
while(i<j){
while(i<j && nums[j]>pivot){
j--;
}
while(i<j && nums[i]<pivot){
i++;
}
if(i<j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
nums[left] = nums[j];
nums[j] = pivot;
return j;
}
//短小写法, 为什么要从右边开始见https://www.jianshu.com/p/72903bf03d5e?from=timeline
public static void quickSortDual(int[] arr, int left, int right) {
if (left > right)
return;
int pivot = arr[left];//基准值
int i = left;//左侧"哨兵"
int j = right;//右侧"哨兵"
while (i<j) {//注意:要从基准值所在侧的另外一侧开始
while (arr[j] >= pivot && i < j)//如果右侧出现了比基准值小的元素,则"哨兵"j停留
j--;
while (arr[i] <= pivot && i < j)//如果左侧出现了比基准值小的元素,则"哨兵"i停留
i++;
if (i < j) {//如果"哨兵"i与j没有相遇则交换其所在位置的数据
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
//此时"哨兵"i与j相遇,交换基准值与该相遇点的数据
arr[left] = arr[i];
arr[i] = pivot;
quickSortDual(arr, left, i - 1);//递归的处理左侧数据
quickSortDual(arr, i + 1, right);//递归的处理右侧数据
}
// 测试
public static void main(String[] args) {
int[] nums = {3,2,4,5,1,5,8,6};
// int[] nums = {7,6,5,4,3,2,2,1};
System.out.println(Arrays.toString(nums));
Sort sort = new Sort();
sort.quickSort(nums,0,nums.length-1);
System.out.println(Arrays.toString(nums));
for(int a:nums)
System.out.println(a);
}
}
import random
## 2路快排
def TWOqsort(arr):
r=len(arr)-1
twoqsort(arr,0,r)
return arr
def twoqsort(arr,l,r):
if l>=r:
return arr
p=partition2(arr,l,r)
twoqsort(arr,l,p-1)
twoqsort(arr,p+1,r)
def partition2(arr,l,r):
## 优化之一,随机选择基准值
tmp=random.randint(l,r)
arr[l],arr[tmp]=arr[tmp],arr[l]
v=arr[l]
i,j=l+1,r
while 1:
## 这里arr[i]<v和后面的arr[j]>v没有等号
# 是因为有连续相等情况时,等号会造成两颗子树不平衡
while i<=r and arr[i]<v:
i+=1
while j>=l+1 and arr[j]>v:
j-=1
## 这里我觉得i>j和i>=j都可
if i>=j:
break
arr[i],arr[j]=arr[j],arr[i]
i+=1
j-=1
arr[l],arr[j]=arr[j],arr[l]
return j
优化1:
基准值的选择,随机选一,或三数取中/九数取中
优化2:
数组较小的时候,使用直接插入排序
优化3:
对递归的优化,尾递归???
## 三路快排
def QuickSortThreeWays(arr):
r=len(arr)-1
quicksortThreeWays(arr,0,r)
return arr
def quicksortThreeWays(arr,l,r):
if l>=r:
return
## 优化之一,随机选择基准值
tmp=random.randint(l,r)
arr[l],arr[tmp]=arr[tmp],arr[l]
v=arr[l]
i,lt,gt=l+1,l,r+1
while i<gt:
if arr[i]<v:
arr[lt+1],arr[i]=arr[i],arr[lt+1]
lt+=1
i+=1
elif arr[i]>v:
arr[gt-1],arr[i]=arr[i],arr[gt-1]
gt-=1
else:
i+=1
arr[l],arr[lt]=arr[lt],arr[l]
quicksortThreeWays(arr,l,lt-1)
quicksortThreeWays(arr,gt,r)
## 快排的非递归形式
def QsortIterative(arr):
n=len(arr)
s=[0,n-1]
while s:
r=s.pop()
l=s.pop()
p=partition2(arr,l,r)
if l<p-1:
s.extend([l,p-1])
if p+1<r:
s.extend([p+1,r])
return arr
https://www.cnblogs.com/alantu2018/p/8465884.html
三路快排
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q63b5y1b-1600823217275)(assets/绘图1-1587877301701.jpg)]
归并排序
用分治法将两个或多个有序序列合并成一个有序序列。
二路归并是 将数组的n个元素看成n个有序子序列;把当前序列组里的子序列两两合并,使两个有序的子序列变成一个有序的子序列,完成一遍后序列数减半,每个子序列的长度加倍;对长度加倍的子序列重复以上操作,最终得到一个长度为n的有序序列。
latest go版本
var temp []int
func sortArray(nums []int) []int {
temp = make([]int, len(nums))
MergeSort(nums, 0, len(nums)-1)
return nums
}
func MergeSort(nums []int, l, r int) {
if l >= r {
return
}
m := (l + r) / 2
MergeSort(nums, l, m)
MergeSort(nums, m+1, r)
Merge(nums, l, m, r)
}
func Merge(nums []int, l, m, r int) {
i, j := l, m+1
for p := l; p <= r; p++ {
if i == m+1 {
temp[p] = nums[j]
j++
} else if j == r+1 {
temp[p] = nums[i]
i++
} else if nums[i] < nums[j] {
temp[p] = nums[i]
i++
} else {
temp[p] = nums[j]
j++
}
}
for p := l; p <= r; p++ {
nums[p] = temp[p]
}
}
private int[] help;
public void mergeSortTotal(int[] nums, int left, int right){
help = new int[nums.length];
mergeSort(nums, left, right);
}
public void mergeSort(int[] nums, int left, int right){
if (left>=right){
return;
}
int m = left + (right-left)/2;
mergeSort(nums, left, m);
mergeSort(nums, m+1, right);
merge(nums, left, m, right);
}
public void merge(int[] nums, int left, int m, int right){
if(right-left<1){
return;
}
System.arraycopy(nums, left, help, left, right-left+1);
int i = left;
int j = m+1;
for(int k=left; k<=right; k++){
if (i>m){
nums[k] = help[j++];
}else if(j>right){
nums[k] = help[i++];
}else if(help[i]<help[j]){
nums[k] = help[i++];
}else{
nums[k] = help[j++];
}
}
}
堆排
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个大顶堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
具体是构建第一个最大堆时,对第一个非叶子节点及其前面的节点依次做ShiftDown操作构建成一个最大堆,将第一个元素与最后一个元素交换,对剩下的n-1个元素,只需要对第一个节点做ShiftDown操作即可构建成一个最大堆取到次小节点,这样依次进行下去直到数组有序。
public void shiftDown(int[] nums, int start, int end){
int i = start;
int j;
int temp=nums[i];
while(2*i+1 <= end){
j = 2*i+1;
if(j+1<=end && nums[j+1]>nums[j]){
j++;
}
if(nums[j]>temp){ //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
nums[i] = nums[j];
i = j;
}else{
break;
}
}
nums[i] = temp;//将temp值放到最终的位置
}
public void HeapSort(int[] nums){
int end = nums.length-1;
for(int i=(end-1)/2;i>=0;i--){
shiftDown(nums,i,end);
}
int temp;
for(int i=end;i>0;i--){
temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
shiftDown(nums, 0,i-1);
}
}
大顶堆: 所有节点的值,大于其左右孩子的值。除了根节点是最大值外,整个数组时近乎有序而不是完全有序的。
ShiftUp: 在最大(小)堆里插入一个元素,放在数组的末尾,依次和其父节点比较,向上的比较/交换。
ShiftDown: 最大堆的数组头取出最大的元素后,将数组末尾的元素放到数组头,依次与其子节点的值进行比较,直到变成一个最大堆。
latest go版本
func sortArray(nums []int) []int {
heapSort(nums)
return nums
}
func heapSort(nums []int) []int {
end := len(nums) - 1
for i := end / 2; i >= 0; i-- {
sink(nums, i, end)
}
for i := end; i >= 0; i-- {
nums[0], nums[i] = nums[i], nums[0]
sink(nums, 0, i-1)
}
return nums
}
func sink(heap []int, root, end int) {
for {
child := root * 2 + 1
if child > end {
return
}
if child < end && heap[child] <= heap[child + 1] {
child++
}
if heap[root] > heap[child] {
return
}
heap[root], heap[child] = heap[child], heap[root]
root = child
}
}
堆排序
借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。实现堆排序需要解决两个问题:
-
如何由一个无序序列建成一个堆?
-
如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
堆(二叉堆)可以视为一棵完全的二叉树,完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
其他排序方法
插入排序
主要是不断的把元素插入到已排序的序列中,最终得到排序序列。具体是,只含第一个元素的序列是排好序的,取未排序序列的第一个元素,它的值记为x,从这个位置向左遍历,如果当前元素的值大于x,将它后移一位,直到空出一位,用x填充,这样就将x插入到了适合的位置,组成了新的有序序列;接下来,按照同样的方法,有序序列不断生长,最后得到排序序列。
复杂度:外层循环总是进行n-1, 数组逆序的最坏情况下,内存循环执行次数分别是1,2,…,n-1,循环次数一共是(n-1)*n/2,复杂度是O(N**2)
稳定性:由于内层循环移动元素只有>x才后移,所以是稳定的。
## 插入排序`
def insertSort(arr):
n=len(arr)
for j in range(1,n):
x=arr[j]
while j < 0 and arr[j-1] < x:
arr[j] = arr[j-1]
j -= 1
arr[j] = x
return arr
选择排序
主要是顺序扫描序列中的元素,记住遇到的最小元素。每一次都对未排序的序列,找到最小元素,与未排序序列的第一个元素交换。
复杂度:循环次数是固定的 (n-1)+…+2+1 , 复杂度O(N**2)
稳定性:由于最小的数会与未排序序列的第一位交换,所以不稳定。
## 选择排序
def SelectionSort(arr):
n=len(arr)
for i in range(n):
minIndex=i
for j in range(i+1,n):
if arr[j]<arr[minIndex]:
minIndex=j
if minIndex!=i:
arr[i],arr[minIndex]=arr[minIndex],arr[i]
return arr
冒泡排序
主要是通过交换元素消除逆序来实现排序。具体是,扫描整个序列,发现相邻的逆序对就交换他们,这样最后一个元素就是最大的,然后除开最后一个元素,对剩下的序列进行扫描和逆序对交换。反复比较和交换,完成整个序列的排序。
改进版:记下交换的位置,这样下一次就只对最后一次交换位置前面的序列进行遍历。
稳定性:稳定,因为只有相邻两个元素是逆序才交换这两个相邻位置。
复杂度:循环次数固定,复杂度O(N2)。改进版本,最好复杂度是O(N),最坏仍是O(N2)
## 冒泡排序
def bubuleSort(arr):
n = len(arr)
for i in range(n-1):
for j in range(n-i-1):
if arr[j+1] < arr[j]:
arr[j+1], arr[j] = arr[j], arr[j+1]
return arr
def bubbleSort2(arr):
n=len(arr)
## 记录每次遍历后最后一次交换的位置newn
newn=1
while newn>0:
newn=0
for i in range(n-1):
if arr[i]>arr[i+1]:
arr[i],arr[i+1]=arr[i+1],arr[i]
newn=i
n=newn+1
return arr
希尔排序
插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。
希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。
归并排序
使用了递归分治的思想,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。
计数排序
如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要立刻说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。但是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。
(java中有整型的最大最小值可以直接用)
int MAX = Integer.MAX_VALUE;
int MIN = Integer.MIN_VALUE;
桶排序
假设有一组长度为N的待排关键字序列K[1…n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。
基数排序
一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。
二分法插入排序
二分法插入排序是在插入第i个元素时,对前面的0~i-1元素进行折半,先跟他们中间的那个元素比,如果小,则对前半再进行折半,否则对后半进行折半,直到left<right,然后再把第i个元素前1位与目标位置之间的所有元素后移,再把第i个元素放在目标位置上。
二分插入排序是稳定的与二分查找的复杂度相同;
最好的情况是当插入的位置刚好是二分位置所用时间为O(log₂n);
最坏的情况是当插入的位置不在二分位置所需比较次数为O(n),无限逼近线性查找的复杂度。
希尔排序
改变步长的naivee版本直接插入排序,让数据跳跃的交换,间隔gap每次取为1/2 (或者依次3-2-1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mvFPqeJF-1600823217277)(assets/v2-8503be998026b904d2d1a7227015c655_b.webp)]
参考:https://zhuanlan.zhihu.com/p/68672733
各排序方法代码(Python)
## 希尔排序
# 比如,21个数,分隔区间为10,5,2,1分别进行排序
def ShellSort(arr):
n=len(arr)
gap=n//2
while gap>=1:
# j是比较的次数
for j in range(gap,n):
i=j
while i-gap>=0:
if arr[i-gap]>arr[i]:
arr[i-gap],arr[i]=arr[i],arr[i-gap]
i-=gap
gap//=2
return arr
import random
## 2路快排
def TWOqsort(arr):
r=len(arr)-1
twoqsort(arr,0,r)
return arr
def twoqsort(arr,l,r):
if l>=r:
return arr
p=partition2(arr,l,r)
twoqsort(arr,l,p-1)
twoqsort(arr,p+1,r)
def partition2(arr,l,r):
## 优化之一,随机选择基准值
tmp=random.randint(l,r)
arr[l],arr[tmp]=arr[tmp],arr[l]
v=arr[l]
i,j=l+1,r
while 1:
## 这里arr[i]<v和后面的arr[j]>v没有等号
# 是因为有连续相等情况时,等号会造成两颗子树不平衡
while i<=r and arr[i]<v:
i+=1
while j>=l+1 and arr[j]>v:
j-=1
## 这里我觉得i>j和i>=j都可
if i>=j:
break
arr[i],arr[j]=arr[j],arr[i]
i+=1
j-=1
arr[l],arr[j]=arr[j],arr[l]
return j
## 三路快排
def QuickSortThreeWays(arr):
r=len(arr)-1
quicksortThreeWays(arr,0,r)
return arr
def quicksortThreeWays(arr,l,r):
if l>=r:
return
## 优化之一,随机选择基准值
tmp=random.randint(l,r)
arr[l],arr[tmp]=arr[tmp],arr[l]
v=arr[l]
i,lt,gt=l+1,l,r+1
while i<gt:
if arr[i]<v:
arr[lt+1],arr[i]=arr[i],arr[lt+1]
lt+=1
i+=1
elif arr[i]>v:
arr[gt-1],arr[i]=arr[i],arr[gt-1]
gt-=1
else:
i+=1
arr[l],arr[lt]=arr[lt],arr[l]
quicksortThreeWays(arr,l,lt-1)
quicksortThreeWays(arr,gt,r)
## 快排的非递归形式
def QsortIterative(arr):
n=len(arr)
s=[0,n-1]
while s:
r=s.pop()
l=s.pop()
p=partition2(arr,l,r)
if l<p-1:
s.extend([l,p-1])
if p+1<r:
s.extend([p+1,r])
return arr
## 非原地堆排,已经不用
## SiftUp插入元素,ShiftDown
## 原地堆排
def HeapSort(arr):
k=len(arr)-1
m=(k+1)//2
for i in range(m,-1,-1):
ShiftDown(arr,i,k)
for k in range(len(arr)-1,0,-1):
arr[k],arr[0]=arr[0],arr[k]
ShiftDown(arr,0,k-1)
return arr
# i是当前节点,k是Down比较的最后一个节点
def ShiftDown(arr,i,k):
while 2*i+1<=k:
j=2*i+1
if j+1<=k and arr[j+1]>arr[j]:
j+=1
if arr[j]<=arr[i]:
break
arr[i],arr[j]=arr[j],arr[i]
i=j
## 归并排序
def MergeSort(arr):
n=len(arr)
mergesort(arr,0,n-1)
return arr
def mergesort(arr,l,r):
if l>=r:
return
m=(l+r)//2
mergesort(arr,l,m)
mergesort(arr,m+1,r)
merge(arr,l,m,r)
# 对相邻区间合并
def merge(arr,l,m,r):
aux=arr[l:r+1]
i,j=l,m+1
for k in range(l,r+1):
if i>m:
arr[k]=aux[j-l]
j+=1
elif j>r:
arr[k]=aux[i-l]
i+=1
elif aux[i-l]<aux[j-l]:
arr[k]=aux[i-l]
i+=1
else:
arr[k]=aux[j-l]
j+=1
## 归并的非递归形式
def MergeSortIterative(arr):
n=len(arr)
s=1
## 虽然这里可以是s<=n,但是我觉得<更好些
while s<n:
i=045
while i+s<n:
merge(arr,i,i+s-1,min(i+2*s-1,n-1))
i+=2*s
s*=2
return arr
import copy
if __name__=='__main__':
cnt=1
while 1:
## 随机输入
if cnt==1:
arr = random.sample(range(1,1000), 100)
## 自定义输入
else:
arr=list(map(int,input().strip().split()))
cnt+=1
print('bubble:',BubbleSort(copy.deepcopy(arr)))
print('twowayqsort:',TWOqsort(copy.deepcopy(arr)))
print('QuickSortThreeWays:',QuickSortThreeWays(copy.deepcopy(arr)))
print('QsortIterative:',QsortIterative(copy.deepcopy(arr)))
print('SelectionSort:',SelectionSort(copy.deepcopy(arr)))
print('HeapSort:',HeapSort(copy.deepcopy(arr)))
print('InsertionSort:',InsertionSort(copy.deepcopy(arr)))
print('ShellSort:',ShellSort(copy.deepcopy(arr)))
print('MergeSort:',MergeSort(copy.deepcopy(arr)))
print('MergeSortIterative:',MergeSortIterative(copy.deepcopy(arr)))
二分查找
应用场景举例:所有的客户账号保存在一份白名单当中。银行输入用户账号名,检查改账号是否存在,每一次查找账号是否在白名单时,就可以使用二分查找。