部分查找和排序
参考
- 《2019年数据结构考研复习指导》王道论坛·组编,电子工业出版社
- 百度百科:直接插入排序
文章目录
1. 查找
(1)折半查找
- 折半查找,又称为二分查找,它仅适用于有序的顺序表。
function binary_search(arr, target) {
var low = 0, high = arr.length - 1, mid; // high是数组长度-1,因为这是最后一位
var count = 0;
while (low <= high) { // 等于的情况也可以,最后有可能会出现指向同一个元素
count++;
mid = Math.floor((low + high) / 2); // 如果直接除以2,出来是小数,计算失败
if (arr[mid] == target) { // 先判断等不等于要找的数再进行下面的流程
console.log(count); // 3
return mid;
} else if (arr[mid] > target) { // 如果中间值大于要找的数,那么就往左找
high = mid - 1;
} else { // 如果中间值小于要找的数,那么就往右找
low = mid + 1;
}
}
console.log(count);
return -1;
}
(function main() {
var arr = [7, 10, 13, 16, 29, 32, 33, 37, 41, 43];
var target = 41;
var result = binary_search(arr, target);
console.log(result); // 8
}());
2. 排序
(1)插入排序
① 直接插入排序
在排序过程中,待排序表L[1…n]在某次排序过程中的某一时刻状态如下:(递增有序序列)
为了实现将元素L(i)插入到已有序的子序列L[1…i-1]中,我们需要执行以下操作(为避免混淆,下面用“L[ ]”表示一个表,而用“L( )”表示一个元素):
- 查找出L(i)在L[1…i]中的插入位置k。
- 将L[k…i-1]中所有元素全部后移一个位置。
- 将L(i)复制到L(k)。
// 获得递增序列
function insertSort(arr, n) {
for (var i = 1; i < n; i++) { // 因为每一次都要跟前一个元素比,所以要从1开始
if (arr[i] < arr[i - 1]) { // 因为默认是递增的,所以如果当前元素比前一个元素小
var temp = arr[i]; // 那么就需要把当前元素插入到前面的有序列表中
var j;
// j--的时候要注意j始终大于等于0
for (j = i - 1; (j >= 0) && (temp < arr[j]); j--) { // 从前一个元素开始
arr[j + 1] = arr[j]; // 把它前面的元素全部往后移,直到当前元素比移动的元素大为止
}
arr[j + 1] = temp; // 当前元素比要移动的元素大,所以插入到要移动的元素后面,即j+1
}
}
console.log(arr); // [1, 12, 14, 34, 67, 76, 80, 89, 98, 109, 190]
}
(function main() {
var arr = [98, 76, 109, 34, 67, 190, 80, 12, 14, 89, 1];
var n = arr.length;
insertSort(arr, n);
}());
② 折半插入排序
先折半查找出元素的待插入为止,然后再统一地移动待插入为止之后的所有元素
// 获得递增序列
function binary_insert(arr, n) { // 折半查找+插入排序
var low, high, mid;
for (var i = 1; i < n; i++) {
var temp = arr[i]; // 不用像直接插入排序那样让当前元素跟前一个元素比较
low = 0;
high = i - 1;
while (low <= high) {
mid = Math.floor((low + high) / 2); // 折半查找
if (arr[mid] > temp) high = mid - 1; // 往左边找
else low = mid + 1; // 往右边找
}
// 因为前面折半查找到了最后一次循环时是low和high和mid都相等的时候
// 1. 如果定位的数字比待插入的数字大,high会在定位数字的左边,high+1正好是定位位置
// 把定位位置开始的数字后移,将待插入数字放在定位位置
// 2. 如果定位的数字比待插入的数字小,high+1就是待插入数字的位置,不会移动
for (var j = i - 1; j >= high + 1; j--) { // high+1就是找到那个位置的后一个
arr[j + 1] = arr[j];
}
arr[high + 1] = temp;
}
console.log(arr); // [1, 12, 14, 34, 67, 76, 80, 89, 98, 109, 190]
}
(function main() {
var arr = [98, 76, 109, 34, 67, 190, 80, 12, 14, 89, 1];
var n = arr.length;
binary_insert(arr, n);
}());
(2)交换排序
① 冒泡排序
假设待排序表长为n,从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1] > A[i]),则交换它们,直到序列比较完。
// 获得递增序列
function bubbleSort(arr, n) {
var i, j;
for (i = 0; i < n - 1; i++) { // 最后一个不用排,因为到了第n-2轮的时候,前n-2个包括第n-2个已经排好
var flag = false;
// 如果写成for (j = i + 1; j < n; j++)就只能排完后半部分的序列
for (j = n - 1; j > i; j--) { // 每换完一轮,前i个数包括第i个都排好的
if (arr[j] < arr[j - 1]) { // 交换
var temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
flag = true;
}
}
if (!flag) break; // 如果这趟遍历下来没有发生交换,就说明已经排好了
}
console.log(arr);
}
(function main() {
var arr = [98, 76, 109, 34, 67, 190, 80, 12, 14, 89, 1];
var n = arr.length;
bubbleSort(arr, n);
}());
② 快速排序
基本思想是基于分治法的:在待排序表L[1…n]中任取一个元素pivot作为基准,通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中所有元素小于pivot,L[k+1…n]中所有元素大于或等于pivot,则pivot放在了其最终位置L(k)上,这个过程称作一趟快速排序。而后分别递归地对两个字表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
// 获得递增序列
function quickSort(arr, low, high) {
if (low < high) { // 递归出口,不能取等于,因为后面的quickSort()执行起来没意义
var pivot = partition(arr, low, high); // 划分子表
quickSort(arr, low, pivot - 1); // 对pivot左边的子表进行排序
quickSort(arr, pivot + 1, high); // 对pivot右边的子表进行排序
}
}
function partition(arr, low, high) {
var pivot = arr[low]; // 取表中第一个元素作为枢轴值
while (low < high) {
// 比枢轴值大的放右边,从后往前遍历时,如果遇到比枢轴值小的就要往前放
// 因为第一个元素已经存成pivot了,所以可以直接把数值放到第一位
while ((low < high) && (arr[high] >= pivot)) high--; // 遇到小过pivot的马上停下
arr[low] = arr[high];
// 比枢轴值小的放左边,从前往后遍历时,如果遇到比枢轴值大的就要往后放
// 因为上面那句代码已经把high位置的元素放在了low位置,所以high位置可以放另一个元素
// 如果整个arr是有序的话也不会影响,因为上一句把high位置的元素放low位置,后面又换回去了
while ((low < high) && (arr[low] <= pivot)) low++; // 遇到打过大过pivot的马上停下
arr[high] = arr[low];
}
// 因为前面存起了low位置的pivot,而while里面最后一次交换是将low位置的元素放到high位置
// 所以这时要把pivot放回low位置
arr[low] = pivot;
return low; // 以low为枢轴划分子表
}
(function main() {
var arr = [98, 76, 109, 34, 67, 190, 80, 12, 14, 89, 1];
var n = arr.length;
quickSort(arr, 0, n - 1);
console.log(arr); // [1, 12, 14, 34, 67, 76, 80, 89, 98, 109, 190]
}());
(3)选择排序
① 简单选择排序
假设排序表为L[1…n],第i趟排序即从L[1…n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可以使得整个排序表有序。
function selectSort(arr, n) {
for (var i = 0; i < n - 1; i++) { // 最后一个数字不用排,因为前面已经排好了
var min = i;
for (var j = i + 1; j < n; j++) { // 从当前值的下一个数开始比较
if (arr[j] < arr[min]) min = j; // 找到最小值的位置
}
if (min != i) { // 如果当前i位置不是最小值所在的位置,那么就要交换
var temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
(function main() {
var arr = [98, 76, 109, 34, 67, 190, 80, 12, 14, 89, 1];
var n = arr.length;
selectSort(arr, n);
console.log(arr); // [1, 12, 14, 34, 67, 76, 80, 89, 98, 109, 190]
}());
(4)归并排序
① 2-路归并排序
假定待排序表含有n个记录,则可以看成是n个有序的子表,每个子表长度为1,然后两两归并,得到 ⌈ n / 2 ⌉ \left \lceil n/2 \right \rceil ⌈n/2⌉个长度为2或1的有序表:再两两归并,如此重复,直到合并成一个长度为n的有序表为止。
function mergeSort(arr, low, high) {
if (low < high) { // 因为要不停划分左右子表,如果low = high证明划分完成
var mid = Math.floor((low + high) / 2); // 划分左右子表
mergeSort(arr, low, mid); // 对左侧进行递归排序
mergeSort(arr, mid + 1, high); // 对右侧进行递归排序
merge(arr, low, mid, high); // 归并
}
}
function merge(arr, low, mid, high) {
var brr = [];
for (var k = low; k <= high; k++) { // 先把arr中的元素全部复制到brr里面
brr[k] = arr[k];
}
for (var i = low, j = mid + 1, k = i; i <= mid&&j <= high; k++) { // 左右子表进行遍历
if (brr[i] <= brr[j]) { // 每一次都是把左右子表中小的那个数放入arr中
arr[k] = brr[i++];
} else {
arr[k] = brr[j++];
}
}
// 下面这个两个while只会执行其中一个
// 上面的选择较小值的for循环,是直到左表下标超了mid或者右表下标超了high才会停下来
// 所以,肯定有一个表已经复制完成,所以只会执行其中一个while循环
while (i <= mid) arr[k++] = brr[i++]; // 如果第一个表没有检测完,则复制
while (j <= high) arr[k++] = brr[j++]; // 如果第二个表没有检测完,则复制
}
(function main() {
var arr = [98, 76, 109, 34, 67, 190, 80, 12, 14, 89, 1];
var n = arr.length;
mergeSort(arr, 0, n-1);
console.log(arr); // [1, 12, 14, 34, 67, 76, 80, 89, 98, 109, 190]
}());
(5)性能总结
3. 补充一个哈夫曼树
(1)定义
在含有N个带权叶子结点的二叉树中,其中带权路径长度(WPL)最小的二叉树成为哈夫曼树,也成为最优二叉树。
WPL = 7x2 + 5x2 + 2x2 + 4x2 = 36
(2)构建
给定N个权值分别为w1,w2,…,wn的结点。
- 将这N个结点分别作为N棵仅含一个结点的二叉树,构成森林F。
- 构造一个新结点,并从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
- 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
- 重复步骤2和3,直至F中只剩下一棵树为止。
例,有六个点abcdef,权值分别为5,9,12,13,16,45,构建哈夫曼树并且算出WPL
WPL = 1x45 + 3x(13+12+16) + 4x(5+9) = 224
分解:
- 先从六个点中找到权值最小的两个点,f(5)和e(9),它们的父节点为14
- 再从剩下的四个点和父节点14中找出两个权值最小的点,c(12)和b(13),它们的父节点为25
- 再从剩下的两个点和两个父节点14和25中找出两个权值最小的点,结点14和d(16),它们的父节点为30
- 再从剩下的一个点和两个父节点25和30中找到两个权值最小的点,结点25和结点30,它们的父节点为55
- 将剩下的一个点a(45)和结点55组合,它们的父节点为100