一张图概括
核心思想
在一个排序算法中,需要通过循环对元素的位置进行调整,因此 每次循环时,算法所完成的任务可以抽象化为将数据从状态A转移到状态B,同时状态B也是下一次转移的起始状态。
因此,抓住一个排序时的循环不变式,保持这个条件交换或者移动其他元素,就能证明一个排序算法的正确性 参考算法导论的定义,一个循环不变式有以下几个性质
- 循环的第一次迭代前位置
- 如果循环的某次迭代之前为真,那么下次迭代前依然为真
- 在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的。
选择排序
- 先在未排序的序列中找到最小(大)元素,存放在排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复步骤2直到排序完成
JS实现
function selectionSort(arr){
let len = arr.length;
let minIndex,temp // 用于存储未排序的位置和交换用的之
// 默认第一个元素是排好序的,遍历len-1个另外的
for(let i=0;i<len-1;i++){
minIndex = i; // 标记当前未排序序列的第一个值
for(let j = i+1;j<len;j++){
// 记录未排序序列中最小元素的位置
if(arr[j]<arr[minIndex]){
minIndex = j;
}
}
//
temp = arr[j];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
冒泡排序
反复交换没有按次序排列的元素,每一次冒泡都可以将一个最大或者最小的元素放在最前或者最后
function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len - 1; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) { // 相邻元素两两对比
var temp = arr[j+1]; // 元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
优化冒泡排序
用一个flag标记是否循环
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
for (int i = 1; i < arr.length; i++) {
// 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = false;
}
}
if (flag) {
break;
}
}
return arr;
}
}
归并排序
核心思想:划分子序列直到不能划分为止,对每个子序列进行排序后合并
分治: 将数组换分为两个规模为n/2的子数组 递归: 递归地对子数组进行排序 合并: 递归地合并两个子数字
有两种实现的思路
- 自上而下进行递归
- 自下而上实现迭代
实现
- 申请空间 大小为两个已经排序的序列大小之和,存放合并后的序列
- 使用两个指针记录这两个已经排序的序列的起始位置
- 比较两个指针指向的元素,选择这两个中较小的一个放入合并空间中,指针后移
- 重复步骤3直至其中一个指针到末尾
- 处理剩余元素
JS实现
function mergeSort(arr) {
let len = arr.length;
if (len < 2) {
return arr;
}
// 分治
let middle = Math.floor(len / 2);
// 子问题求解
let left = arr.slice(0, middle);
let right = arr.slice(middle);
function merge(left, right) {
// 开辟一个和原始序列一样大小的新空间
var result = [];
// 当两个指针均没有移动到最后
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift())
}
}
// 处理剩余
while (left.length)
result.push(left.shift());
while (right.length)
result.push(right.shift());
return result;
}
}
快速排序
同样基于递归分治思路
- 分解:将数组划分为两个子数组(可以为空),挑选其中一个元素作为基准(pivot),对序列进行重新排序,所有被基准值小的元素放到基准前,比基准值大的放在基准后,实现分区。
- 子问题解决: 递归地对两个子数组进行排序
- 合并:原地排序无须合并
时间复杂度
- 最坏情况:每次划分的数组都是n-1个元素和0个元素,划分操作的时间复杂度为 整体的时间复杂度
- 最好情况: 划分时均分
- 平均情况 常数比例的划分方法产生深度为 递归树 每层代价为
JS实现
function quickSort (arr,left,right){
let len = arr.length;
let partitionIndex; // 分割的index
// 增加如果没传入的判断
let left = typeof left == 'number'?0:left;
let right = typeof right == 'number'?len-1:right;
if(left<right){
partitionIndex = partition(arr,left,right);
// 分治排序
quickSort(arr,left,partitionIndex-1); // 排序基准左侧的子序列
quickSort(arr,partitionIndex+1,right);
}
return arr;
}
function partition(arr,left,right){
// 选最左侧点作为排序基准点
let pivot = left;
let index = pivot+1;// 用于遍历基准意外的元素
for(let i=index;i<right;i++){
// 如果遍历到的元素小于基准值则将其与排序序列的最左侧元素互换,说明i之前一定有元素大于基准值
if(arr[i]<arr[pivot]){
swap(arr,i,index);
index++;
}
}
// 将结束值与遍历到i互换,让i位于数列的中间 left--j小于基准 j+2---right 大于基准
swap(arr,i,right);
// 返回基准点的位子
return index-1;
}
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 快速排序的第二种方法
function partition2(arr,low,high){
let pivot = arr[low]; // 先选择第一个元素作为基准值
// 使用两个指针双向地向中间遍历
while(low<high){
whilie(low<high && arr[high]>pivot){
// 如果右侧指针大于基准值就正常移动
--high;
}
// 不满足时说明找到了比基准值小元素 将其与左侧指针值互换,保证该值位于基准左侧
arr[low] = arr[high];
// 互换以后左侧指针开始移动 逻辑相同 将比基准值大的点移动到基准值右侧
while (low < high && arr[low] <= pivot) {
++low;
}
arr[high] = arr[low];
}
// 记得还原基准值的位置
arr[low] = pivot
return arr
}
function quickSort2(arr,low,high){
if(low<high){
let pivot = partition2(arr,low,high);
quickSort2(arr,low.pivot-1);
quickSort2(arr,pivot+1,high);
}
return arr;
}
快速选择算法
const findKthLargest = (nums, k) => {
return quickSelect(nums, 0, nums.length - 1, k);
};
const quickSelect = (nums, lo, hi, k) => {
// 避免最坏情况发生
const p = Math.floor(Math.random() * (hi - lo + 1)) + lo;
swap(nums, p, hi);
// 声明两个指针从左向右遍历
let i = lo;
let j = lo;
// 将最右侧点视为pivot 遍历交互小于基准值的点到基准的左侧
while (j < hi) {
if (nums[j] <= nums[hi]) {
swap(nums, i++, j);
}
j++;
}
// 注意复原基准值的位置
swap(nums, i, j);
// pivot 是我们要找的 Top k
if (hi === k + i - 1) return nums[i];
// Top k 在右边
if (hi > k + i - 1) return quickSelect(nums, i + 1, hi, k);
// Top k 在左边
return quickSelect(nums, lo, i - 1, k - (hi - i + 1));
};
const swap = (nums, i, j) => ([nums[i], nums[j]] = [nums[j], nums[i]]);