左神算法基础班 第2章 第一周学习——认识 O(N*logN) 的排序

本文深入探讨了归并排序的原理和实现,详细解析了其时间复杂度和空间复杂度。此外,还介绍了归并排序在解决小和问题和逆序对问题中的应用。同时,文章涵盖了堆结构与堆排序,解释了大根堆和小根堆的概念,以及堆排序的过程。最后,讨论了荷兰国旗问题的解决方案,展示了如何在限定空间复杂度和时间复杂度内完成排序任务。
摘要由CSDN通过智能技术生成

2.1 归并排序

归并排序动画
  • 时间复杂度为 O(Nlog(N)),额外的空间复杂度为 O(N)
  • 归并排序整体就是一个简单递归, 左边排好序、 右边排好序,再让其整体有序。让其整体有序的过程里用了排外序方法
归并排序变量定义
// 归并, O(Nlog(N))
public void MergeSort (int[] arr, int left, int right) {
	if (left == right) {
		return;
	}
	int mid = left + ((right - left) >> 1);
	MergeSort(arr, left, mid);			// 拆分左半部分
	MergeSort(arr, mid+1, right);		// 拆分右半部分
	Merge(arr, left, right, mid);		// 每次拆分完之后就合并, 合并的都是有序的数组
}

public void Merge (int[] arr, int left, int right, int mid) {
	int[] help = new int[right - left + 1];
	int i = 0;
	int p1 = left;
	int p2 = mid + 1;
	while (p1 <= mid && p2 <= right) {
		if (arr[p1] < arr[p2]) {
			help[i++] = arr[p1++];
		} else {
			help[i++] = arr[p2++];
		}
	}
	while (p1 <= mid) {
		help[i++] = arr[p1++];
	}
	while (p2 <= right) {
		help[i++] = arr[p2++];
	}
	for (i = 0; i < help.length; i++) {
		arr[left+i] = help[i];
	}
}

2.2 归并排序的扩展

2.2.1 小和问题

在一个数组中, 每一个数左边比当前数小的数累加起来, 叫做这个数组的小和。 求一个数组的小和。
例子: [1,3,4,2,5],1左边比1小的数, 没有;3左边比3小的数,1;4左边比4小的数,1、3;2左边比2小的数,1;5左边比5小的数,1、3、4、2;所以小和为 1 + 1 + 3 + 1 + 1 + 3 + 4 + 2 = 16

public int SmallSum (int[] arr, int left, int right) {
	if (left == right) {
		return 0;
	}
	int mid = left + ((right - left) >> 2);
	SmallSum2(arr, left, mid);
	SmallSum2(arr, mid + 1, right);
	return SmallSum2Merge(arr, left, right, mid);
}

static int count = 0;
public int SmallSumMerge (int[] arr, int left, int right, int mid) {
	int[] help = new int[right - left + 1];
	int i = 0;
	int p1 = left;
	int p2 = mid + 1;
	//int res = 0;
	while (p1 <= mid && p2 <= right) {
		if (arr[p1] < arr[p2]) {
			count += (right - p2 + 1) * arr[p1];		// 计算右组有多少个数小于左组arr[p1]这个数
			help[i++] = arr[p1++];
		} else {
			help[i++] = arr[p2++];
		}
	}
	while (p1 <= mid) {
		help[i++] = arr[p1++];
	}
	while (p2 <= right) {
		help[i++] = arr[p2++];
	}
	for (i = 0; i < help.length; i++) {
		arr[left + i] = help[i];
	}
	return count;
}

2.2.2 逆序对问题

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请返回逆序对的数量。

public int InverseOrder2 (int[] arr, int left, int right) {
	if (left == right) {
		return 0;
	}
	int mid = left + ((right - left) >> 1);
	InverseOrder2(arr, left, mid);
	InverseOrder2(arr, mid + 1, right);
	return InverseOrder2Merge(arr, left, right, mid);
}

static int count = 0;
public int InverseOrder2Merge (int[] arr, int left, int right, int mid) {
	int i = 0;
	int p1 = left;
	int p2 = mid + 1;
	int[] help = new int[right - left + 1];
	while (p1 <= mid && p2 <= right) {
		if (arr[p1] > arr[p2]) {            // 逆序排序
			count += (right - p2 +1);		// 不理解, 计算右组中有多少个数小于arr[p1]
			help[i++] = arr[p1++];						
		} else {
			help[i++] = arr[p2++];
		}
	}
	while (p1 <= mid) {
		help[i++] = arr[p1++];
	}
	while (p2 <= right) {
		help[i++] = arr[p2++];
	}
	for (i = 0; i < help.length; i++) {
		arr[left+i] = help[i];
	}
	return count;
}

2.3 堆结构与堆排序

 

用户能往黑盒里随意加入数据,随意读取并删除最大的数据
大根堆示意图

 

十大经典排序算法动画演示
堆排序动画1

 

堆排序动画2

 

堆在程序中可用数组实现,但逻辑上就是一个完全二叉树, 完全二叉树是指最后一层从左向右增加,其他层都是满层的二叉树。 假设二叉数起始位置对应数组下标 0 ,对于 i 节点,它的左孩子是 2i+1,它的右孩子是 2i+2,父节点是 (i-1)/2。 堆可分为大根堆小根堆,完全二叉树中如果每棵子树的最大值都在顶部就是大根堆,如果每棵子树的最小值都在顶部就是小根堆。 如何实现大根堆,用户希望调用 void add (num) 方法得到符合大根堆要求的数组,可以这样考虑:每加入一个数,都和自己的父结点的数做比较,如果比自己的父亲结点的位置大,就交换位置,否则不做交换。 这种从下至上的过程如 HeapInsert 所示:

public void HeapInsert (int[] arr, int index) {
	while (arr[index] > arr[(index - 1) / 2]) {		// (index - 1) / 2) 是 index 的父节点
		Swap(arr, index, (index - 1) / 2);
		index = (index - 1) / 2;
	}
}

int popmax(),用户希望调用这个函数返回堆结构的最大值,并删除掉这个值,剩下的值保持大根堆的结构。 那么可以取出 arr[0] 的值,将 arr 中最后一个值放到 arr[0] 位置上,它有2个子节点,2个孩子作比较,选出大的和它进行比较,若孩子大,则交换位置。 来到新的位置后继续向下进行比较,直到自己的子节点比自己小 or 没有左孩子节点为止。 这种从上到下的过程称为Heapify

// 某个数在index位置,能否往下移动,int popmax()
public void Heapify (int[] arr, int index, int heapSize) {
	int left = index * 2 + 1;
	int right = index * 2 + 2;
	int largest = 0;
	while (left < heapSize) {
		// 两个孩子中,谁的值大,把下标给largest
		if (right < heapSize && arr[left] < arr[right]) {
			largest = right;
		} else {
			largest = left;
		}
		// 父和较大的孩子之间,谁的值大,把下标给largest
		if (arr[largest] < arr[index]) {
			largest = index;
		}
		if (largest == index) {
			break;
		}
		Swap(arr, largest, index);
		index = largest;
		left = index * 2 + 1;
		right = index * 2 + 2;
	}
}

堆排序,时间复杂度为 O(Nlog(N))

// 堆排序, O(Nlog(N))
public void HeapSort (int[] arr) {
	if (arr == null || arr.length < 2) {
		return;
	}
	// 把arr转换成大根堆结构, O(Nlog(N))
	for (int i = 0; i < arr.length; i++) {		// O(N)
		HeapInsert(arr, i);						// O(log(N))
	}
	// 对大根堆进行排序,O(Nlog(N))
	int heapSize = arr.length;
	Swap(arr, 0, --heapSize);
	while (heapSize > 0) { 			    // O(N)
		Heapify(arr, 0, heapSize); 		// O(logN)
		Swap(arr, 0, --heapSize); 		// O(1)
	}
}

2.4 堆排序扩展题目——小范围排序

已知一个几乎有序的数组, 几乎有序是指, 如果把数组排好顺序的话, 每个元素移动的距离可以不超过k, 并且k相对于数组来说比较小。 请选择一个合适的排序算法针对这个数据进行排序。

先跳过,留个坑,回头补。

  • in-place (原地算法):占用常数内存,不占用额外内存,靠输出覆盖输入。
  • out-place:占用额外内存。

2.5 荷兰国旗问题

问题一 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。 数组左边和右边都可以是无序的。 要求额外空间复杂度O(1),时间复杂度O(N)。

问题一结果图

定义指针p1,arr[p1] 及 arr[p1] 左边的数都比 num 小,即 p1 是小于区的右边界。 遍历数组 arr,当 arr[i] <= num 时,交换 arr[p1] 与 arr[i] ,p1++。

public static void NetherlandsFlag (int[] arr, int num) {
	int p1 = 0;
	for (int i = 0; i < arr.length; i++) {
		if (arr[i] <= num) {
			Swap(arr, p1, i);
			p1++;
		}
	}
}

public static void Swap (int[] arr, int i, int j) {
	if (i != j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
	}
}

问题二(荷兰国旗问题)给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。 要求额外空间复杂度O(1),时间复杂度O(N)。

定义双指针p1,p2。 arr[p1] 及 arr[p1] 左边的数都比 num 小,arr[p2] 及 arr[p2] 左边的数 (不包括arr[p1] 及 arr[p1] 左边的数) 等于num。 p1 是小于区的右边界,p2 是等于区的右边界。 遍历数组 arr,当 arr[i] < num 时,交换 arr[p1] 与 arr[i] ,p1++,p2++;当 arr[i] == num 时,交换 arr[p2] 与 arr[i] ,p2++。

荷兰国旗问题结果图
public static void NetherlandsFlag (int[] arr, int num) {
	int p1 = 0;
	int p2 = 0;
	for (int i = 0; i < arr.length; i++) {
		if (arr[i] < num) {
			Swap(arr, p1, i);
			p1++;
			p2++;
		}
		else if (arr[i] == num) {
			Swap(arr, p2, i);
			p2++;
		}
	}
}

public static void Swap (int[] arr, int i, int j) {
	if (i != j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
	}
}

2.6 快速排序

左神讲的快排没有听懂,他找pivot是随机找的,速度更快。最后看的啊哈算法,快排,代码比较简洁。我写的代码把基准数换成数组的最后一位了。

图解快速排序
快排变量设置
快排变量设置
public void quickSort (int[] arr, int left, int right) {
	if(left >= right) {
		return;
	}			
	int p1 = left;							// 左下标
	int p2 = right;							// 右下标
	int pivot = arr[right];					// 基准数, 最后一位		
	
	while (p1 < p2)
	{
		while (arr[p1] <= pivot && p1 < p2)	// 先左
			p1++;
		while (arr[p2] >= pivot && p1 < p2)	// 后右
			p2--;
		if(p1 < p2) {
			swap(arr, p1, p2);
		}
	}		
	// 基准数归位
	arr[right] = arr[p1];
	arr[p1] = pivot;
	
	quickSort(arr, left, p1 - 1);		// 递归左边		// 这时候p1 == p2
	quickSort(arr, p2 + 1, right);		// 递归右边
}

2.7 希尔排序

希尔排序示意图
public static void shellSort (int[] arr) {
    int length = arr.length;
    int temp;
    for (int gap = length / 2; gap >= 1; gap /= 2) {
        for (int i = gap; i < length; i++) {
            temp = arr[i];
            int j = i - gap;
            while (j >= 0 && arr[j] > temp) {
                arr[j + gap] = arr[j];
                j -= gap;
            }
            arr[j + gap] = temp;
        }
    }
}

知道希尔排序的过程,但是代码写的太优雅了,读不懂,之后填坑。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值