目录
本文主要使用Java语言来实现编程中的一些必知必会传统算法。
一、排序
普通排序类算法,可以去[ LintCode 463/464 ]处练习,校对。
1. 冒泡排序
带有flag标志的冒泡排序算法如下:
public class Solution {
/**
* @param A: an integer array
* @return: nothing
*/
public void bubbleSort(int[] A) {
int n = A.length;
for(int i = n-1; i > 0; i--){
boolean f = true;
for(int j = 0; j < i; j++){
if(A[j] > A[j+1]){
int t = A[j];
A[j] = A[j+1];
A[j+1] = t;
f = false;
}
}
if(f) break;
}
}
}
双端冒泡排序(鸡尾酒排序)实现如下。
public class Solution {
public void biBubbleSort(int[] A) {
int n = A.length;
int low = 0, idx = 0;
int high = n - 1;
while(low < high){
for(int i = low; i < high; i++){
if(A[i] > A[i+1]){
int t = A[i];
A[i] = A[i+1];
A[i+1] = t;
idx = i;
}
}
high = idx;
for(int i = high; i > low; i--){
if(A[i] < A[i-1]){
int t = A[i];
A[i] = A[i-1];
A[i-1] = t;
idx = i;
}
}
low = idx;
}
}
}
二者的时间复杂度均为O(N2)。
双端冒泡排序参考自:https://blog.csdn.net/qq_35433716/article/details/83957847
2. 插入排序
以Java为例,代码如下;注意Java中的数组下标从0开始编号。
public class Solution {
public void insertSort(int[] A) {
int n = A.length;
for(int i = 1; i < n; i++){
int t = A[i];
int j = i - 1;
for(; j >= 0 && A[j] > t; j--) A[j+1] = A[j];
A[j+1] = t;
}
}
}
3. 希尔排序
void shell_sort(int arr[], int len) {
int gap, i, j;
int temp;
for (gap = len >> 1; gap > 0; gap >>= 1)
for (i = gap; i < len; i++) {
temp = arr[i];
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
arr[j + gap] = arr[j];
arr[j + gap] = temp;
}
}
使用Java的实现代码如下:
public class Solution {
public void shellSort(int[] A) {
int n = A.length;
int gap, i, j;
int temp;
for(gap = n >> 1; gap > 0; gap >>= 1){
for(i = gap; i < n; i++){
temp = A[i];
for(j = i - gap; j >= 0 && A[j] > temp; j -= gap)
A[j + gap] = A[j];
A[j + gap] = temp;
}
}
}
}
4. 选择排序
选择排序的通用算法框架(C/C++描述)如下:
void selectSort(int arr[])
{
for(int i = 0; i < arr.length; ++i) // 选择第i小的记录,并交换到位
{
int j = selectMin(arr, i); // 在arr[i.. arr.length]中选择key最小的记录
if(i != j) swap(arr[i], arr[j]); // 与第i个记录交换
}
}
使用C/C++更详细的实现如下:
void selectSort(int *arr)
{
for(int i = 0; i < arr.length; ++i){
int minIdx = i;
for(int j = i + 1; j < arr.length; j++) {
if(arr[minIdx] > arr[j]) {
minIdx = j;
}
}
}
if(i != minIdx) swap(arr[i], arr[minIdx]);
}
使用Java的实现代码如下:
public class Solution {
public void selectSort(int[] A) {
int n = A.length;
for(int i = 0; i < n; i++){
int minIdx = i;
for(int j = i + 1; j < n; j++){
if(A[j] < A[minIdx]){
minIdx = j;
}
}
if(i != minIdx){
int t = A[i];
A[i] = A[minIdx];
A[minIdx] = t;
}
}
}
}
5. 快速排序
快排递归版Java实现代码如下:
public class Solution {
public void sortIntegers2(int[] A) {
quickSort(A, 0, A.length-1);
}
private void quickSort(int [] A, int left, int right){
if(left > right) return;
int low = left, high = right;
int key = A[low];
while(left < right){
while(left < right && A[right] >= key) --right;
A[left] = A[right];
while(left < right && A[left] <= key) ++left;
A[right] = A[left];
}
A[right] = key;
quickSort(A, low, left - 1);
quickSort(A, left + 1, high);
}
}
快排非递归版C/C++实现如下:
int partition(int *a, int low, int high){
int p = a[low];
while(low < high){
while(low < high && a[high] >= p) --high;
a[low] = a[high];
while(low < high && a[low] <= p) ++low;
a[high] = a[low];
}
a[low] = p;
return low;
}
void quicksort(int *a, int left, int right){
stack<int> temp;
int i, j;
temp.push(right);
temp.push(left);
while(!temp.empty()){
i = temp.top();
temp.pop();
j = temp.top();
temp.pop();
if(i >= j) continue;
int k = partition(a, i, j);
if(k > i){
temp.push(k-1);
temp.push(i);
}
if(k < j){
temp.push(j);
temp.push(k+1);
}
}
}
快排非递归版Java实现如下:
import java.util.Stack;
public class Solution {
public void sortIntegers2(int[] A) {
quickSort(A, 0, A.length-1);
}
private int partition(int [] A, int low, int high){
int p = A[low];
while(low < high){
while(low < high && A[high] >= p) -- high;
A[low] = A[high];
while(low < high && A[low] <= p) ++ low;
A[high] = A[low];
}
A[low] = p;
return low;
}
private void quickSort(int [] A, int left, int right){
Stack<Integer> temp = new Stack<Integer>();
int i, j;
temp.push(right);
temp.push(left);
while(!temp.empty()){
i = temp.pop();
j = temp.pop();
if(i >= j) continue;
int k = partition(A, i, j);
if(k > i){
temp.push(k-1);
temp.push(i);
}
if(k < j){
temp.push(j);
temp.push(k+1);
}
}
}
}
6. 堆排序
完美二叉树(即:满二叉树),定义从略。完全二叉树:除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐。完满二叉树(Full Binary Tree/Strictly Binary Tree):所有非叶结点的度都是2。
堆排序算法借用了完全二叉树结点编号的一个特征,假设根节点编号为0,这也契合数组的第一个元素下标为0,则左子树编号为2*0+1,右子树编号为2*0+2,一般地,设父结点编号为A,则左子树根节点编号为2*A+1,右子树根结点编号为:2*A+2。
下述代码中,a为待排序数组,在heapAdjust()函数中,数组a[s..m]中除s结点之外均满足堆的定义。
public class Solution {
public void heapsort(int[] A) {
int n = A.length;
if(n == 0) return;
for(int i = n / 2; i >= 0; i--){
heapAdjust(A, i, n-1); // 初始建堆,从最后一个非叶节点开始
}
for(int i = n-1; i > 0; i--){
int t = A[0];
A[0] = A[i];
A[i] = t; // 将堆顶值(数组首元素)交换至数组尾部
heapAdjust(A, 0, i - 1);
}
}
private void heapAdjust(int [] a, int s, int m){
int t = a[s];
int i = 2 * s + 1;
while(i <= m){
if(i < m && a[i] < a[i+1]) ++i; // 找到左右子树根节点的最大值
if(t >= a[i]) break; // 已满足堆定义,无需再向下筛
a[s] = a[i]; // 使s结点满足堆定义
s = i; // 使s结点满足堆定义
i = i * 2 + 1;
}
a[s] = t;
}
// 大顶/根堆调整算法递归版
private void heapAdjust2(int [] a, int s, int m){
int t = a[s];
int i = 2*s + 1;
if(i > m) return;
if(i < m && a[i] < a[i+1]) ++i;
if(t >= a[i]) return;
else{
a[s] = a[i];
a[i] = t;
}
heapAdjust2(a, i, m);
}
}
7. 归并排序
本实现给出递归版与非递归版两种,非递归版归并排序仅仅是mergeSort函数的不同,以C/C++语言为例。
void merge(int *a, int low, int mid, int high){
int i, j, k;
int b[high - low + 1]; // 中间过程数组
i = low; j = mid + 1;
for(k=0; i <= mid && j <= high; k++){
if(a[i]<a[j]) b[k]=a[i++];
else b[k] = a[j++];
}
while(i<=mid) b[k++] = a[i++];
while(j<=high) b[k++] = a[j++];
for(i=low, j=0; i <= high; i++, j++)
a[i] = b[j];
}
void mergeSort(int a[], int low, int high){
if(low < high){
int mid = (low + high) / 2;
mergeSort(a, low, mid);
mergeSort(a, mid + 1, high);
merge(a, low, mid, high);
}
}
// 非递归版的mergeSort2()函数如下
void mergeSort2(int a[], int n){
int step = 1; // 步长分别为1,2,4,8…
while(step <= n){
int low = 0;
while(low + step <= n){
int mid = low + step - 1;
int high = mid + step;
if(high > n) high = n;
merge(a, low, mid, high);
low = high + 1;
}
step *= 2
}
}
当 int a[8] = {1, 6, 3, 2, 9, 8, 7, 5}; 此时,int n = 8;
递归版调用方式为:mergeSort(a, 0, n-1);
非递归调用方式为:mergeSort2(a, n-1);
使用Java实现的归并算法代码如下:
public class Solution {
public void sortIntegers2(int[] A) {
// mergeSort(A, 0, A.length - 1);
mergeSort2(A, A.length - 1);
}
private void mergeSort(int[] a, int low, int high){
if(low < high){
int mid = (low + high) / 2;
mergeSort(a, low, mid);
mergeSort(a, mid + 1, high);
merge(a, low, mid, high);
}
}
private void merge(int[] a, int low, int mid, int high){
int i, j, k;
int[] b = new int[high - low + 1];
i = low;
j = mid + 1;
for(k = 0; i <= mid && j <= high; k++){
if(a[i] < a[j]) b[k] = a[i++];
else b[k] = a[j++];
}
while(i <= mid) b[k++] = a[i++];
while(j <= high) b[k++] = a[j++];
for(i = low, j = 0; i <= high; i++, j++){
a[i] = b[j];
}
}
private void mergeSort2(int[] a, int n){
int step = 1;
while(step <= n){
int low = 0;
while(low + step <= n){
int mid = low + step - 1;
int high = mid + step;
if(high > n) high = n;
merge(a, low, mid, high);
low = high + 1;
}
step *= 2;
}
}
}
8. 桶排序、基数排序、计数排序(从略)
二、 递归/搜索
1. 栈的逆序,递归写法
在考虑递归解法的时候,需要铭记的一点是:发现原问题中的子问题。就本问题而言,子问题就是可以考虑成以下两种形式:
(1) 取出栈顶元素后,将栈进行逆序,最后将取出的栈顶元素插入到栈底;
(2) 取出栈底元素后,将栈进行逆序,最后将取出的栈底元素压入到栈顶。
public class Solution{
public void reverseStack(Stack stack){
if(stack.empty()) return;
int top = stack.pop();
reverseStack(stack);
insert2StackBottom(stack, top);
}
public void insert2StackBottom(Stack stack, int bottom){
if(stack.empty()) stack.push(bottom);
int top = stack.pop();
insert2StackBottom(stack, bottom)
stack.push(top);
}
}
方法insert2StackBottom的子问题为:将栈顶元素取出,将待插入元素插入到栈的栈底,将取出的栈顶元素压入。
本题参考自:https://blog.csdn.net/dm_vincent/article/details/8008238
另有相类似的递归练习题:字符串逆序,逆序打印单链表 等等。
2. 实现汉诺塔 [ LintCode 169 ]
非常经典的递归应用练习题,应深刻理解、掌握、应用。
public class Solution {
/**
* @param n: the number of disks
* @return: the order of moves
*/
private List ans = new ArrayList();
private void hanoi(int n, char x, char y, char z){
if(n == 1){
String t = "from " + x + " to " + z;
ans.add(t);
}
else{
hanoi(n-1, x, z, y);
String t = "from " + x + " to " + z;
ans.add(t);
hanoi(n-1, y, x, z);
}
}
public List<String> towerOfHanoi(int n) {
hanoi(n, 'A', 'B', 'C');
return ans;
}
}
3. 数组的全排列 [ LeetCode 46/47 ]
求数组全排列有递归与迭代两种解法,数组中含与不含重复数字时解法有所不同,这里当前仅辑录递归解法,迭代解法待加。
数组中不含重复数字的解法如下:
class Solution {
private List<List<Integer>> ans = new ArrayList<List<Integer>>();
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
private void getPermutation(int[] arr, int n, int idx){
if(idx == n){
List<Integer> tmp = new ArrayList<Integer>();
for(int e: arr)
tmp.add(e);
ans.add(tmp);
}
else{
for(int i = idx; i < n; i++){
swap(arr, i, idx);
getPermutation(arr, n, idx + 1);
swap(arr, i, idx);
}
}
}
public List<List<Integer>> permute(int[] nums) {
getPermutation(nums, nums.length, 0);
return ans;
}
}
数字中含重复数字的解法如下:
class Solution {
private List<List<Integer>> ans = new ArrayList<List<Integer>>();
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
private void getPermutation(int[] arr, int n, int idx){
if(idx == n){
List<Integer> tmp = new ArrayList<Integer>();
for(int e: arr)
tmp.add(e);
ans.add(tmp);
}
else{
for(int i = idx; i < n; i++){
boolean f = false;
for(int j = i+1; j < n; j++){
if(arr[i] == arr[j]){
f = true;
break;
}
}
if(f) continue;
swap(arr, i, idx);
getPermutation(arr, n, idx + 1);
swap(arr, i, idx);
}
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
getPermutation(nums, nums.length, 0);
return ans;
}
}
三、 链表题
单链表翻转
本题有多种解法,至少有四种解法,修改指针法,栈储存节点法,递归法、修改链表节点值法。这里仅给出递归法。[ LeetCode 206/LintCode 35 ]
优雅版递归解法如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null)
return head;
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
四、 二叉树
1. 前序遍历非递归实现 [ LeetCode 144 ]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
if(root == null) return new ArrayList();
List<Integer> ans = new ArrayList<Integer>();
Stack<TreeNode> st = new Stack<TreeNode>();
TreeNode cur = root;
while(cur != null || !st.empty()){
if(cur != null){
st.push(cur);
ans.add(cur.val);
cur = cur.left;
}
else{
TreeNode t = st.pop();
cur = t.right;
}
}
return ans;
}
}
2. 中序遍历非递归实现 [ LeetCode 094 ]
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return new ArrayList();
List<Integer> ans = new ArrayList<Integer>();
Stack<TreeNode> st = new Stack<TreeNode>();
TreeNode cur = root;
while(cur != null || !st.empty()){
if(cur != null){
st.push(cur);
cur = cur.left;
}
else{
TreeNode t = st.pop();
ans.add(t.val);
cur = t.right;
}
}
return ans;
}
}
3. 后序遍历非递归实现 [ LeetCode 145 ]
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if(root == null) return new ArrayList();
List<Integer> ans = new ArrayList<Integer>();
Stack<TreeNode> st = new Stack<TreeNode>();
TreeNode cur = root;
TreeNode lastVisited = null;
while(cur != null || !st.empty()){
if(cur != null){
st.push(cur);
cur = cur.left;
}
else{
TreeNode t = st.peek();
if(t.right != null && t.right != lastVisited)
cur = t.right;
else{
ans.add(t.val);
st.pop();
lastVisited = t;
}
}
}
return ans;
}
}
4. 给前序和中序,求二叉树
已知:前序 + 中序 => 后序 [LeetCode 105/LintCode 73];
后序 + 中序 => 前序 [LeetCode 106/LintCode 72]。
务必要注意数组切片时其中的下标关系,不能出现丁点错误,否则各种Bug。下面Java代码中的做法不很高效,有更高效的解法,可做尝试。
给前序与中序,求二叉树,代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
import java.util.Arrays;
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null || preorder.length == 0)
return null;
if(inorder == null || inorder.length == 0)
return null;
int tmp = preorder[0];
TreeNode root = new TreeNode(tmp);
int mid = 0;
for(int i = 0; i < inorder.length; i++){
if(tmp == inorder[i]){
mid = i;
break;
}
}
int[] l1 = Arrays.copyOfRange(preorder, 1, mid+1);
int[] r1 = Arrays.copyOfRange(inorder, 0, mid);
root.left = buildTree(l1, r1);
int[] l2 = Arrays.copyOfRange(preorder, mid+1, preorder.length);
int[] r2 = Arrays.copyOfRange(inorder, mid+1, inorder.length);
root.right = buildTree(l2, r2);
return root;
}
}
给中序和后序,求二叉树,代码如下:
import java.util.Arrays;
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
if(inorder == null || inorder.length == 0)
return null;
if(postorder == null || postorder.length == 0)
return null;
int tmp = postorder[postorder.length-1];
TreeNode root = new TreeNode(tmp);
int mid = 0;
for(int i = 0; i < inorder.length; i++){
if(tmp == inorder[i]){
mid = i;
break;
}
}
int[] l1 = Arrays.copyOfRange(inorder, 0, mid);
int[] r1 = Arrays.copyOfRange(postorder, 0, mid);
root.left = buildTree(l1, r1);
int[] l2 = Arrays.copyOfRange(inorder, mid+1, inorder.length);
int[] r2 = Arrays.copyOfRange(postorder, mid, postorder.length-1);
root.right = buildTree(l2, r2);
return root;
}
}
五、 二分法
二分查找的条件,复杂度。返回目标元素的位置,不存在返回-1。
整数的二分查找,如果元素类型任意 [C++而言] 。
旋转数组的二分查找。
假设要查找的元素为key,则拢共有以下6种查找情况,第一个=key的元素,第一个>=key的元素,第一个>key的元素;最后一个=key的元素,最后一个<=key的元素,最后一个<key的元素。
二分查找的条件是数组/列表中的元素具备某种顺序,查找的时间复杂度为O(logN),普通的二分查找(递归/非递归)模板如下,其中查找区间为闭区间[L, R]。其中可以有多个等于key的元素,但只需返回任意一个位置。[ LintCode 457 ]
迭代版二分查找模板如下:
public class Solution {
public int biSearch(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] > key) R = mid - 1;
else if(arr[mid] < key) L = mid + 1;
else return mid;
}
return -1;
}
}
递归版二分查找模板如下:
public class Solution {
private int biSearch(int[] arr, int key, int L, int R){
if(L > R) return -1;
int mid = L + (R-L) / 2;
if(arr[mid] > key) return biSearch(arr, key, L, mid - 1);
else if(arr[mid] < key) return biSearch(arr, key, mid + 1, R);
else return mid;
}
public int findPosition(int[] nums, int target) {
return biSearch(nums, target, 0, nums.length - 1);
}
}
其他形式的二分法可以参考:https://github.com/ZXZxin/ZXBlog/blob/master/%E5%88%B7%E9%A2%98/InterviewAlgorithm.md
我这里也录入于此,以供参考。
第一个=key的元素的下标:
public int firstEqual(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] >= key) R = mid - 1;
else L = mid + 1;
}
if(L < arr.length && arr[L] == key) return L;
return -1;
}
第一个>=key的元素的下标:
public int firstLargeEqual(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] >= key) R = mid - 1;
else L = mid + 1;
}
return L;
}
第一个>key的元素的下标:
public int firstLarge(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] > key) R = mid - 1;
else L = mid + 1;
}
return L;
}
最后一个=key的元素的下标,不存在返回-1:
public int lastEqual(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] <= key) L = mid + 1;
else R = mid - 1;
}
if(R >= 0 && arr[R] == key) return R;
return -1;
}
最后一个<=key的元素的下标:
public int lastSmallEqual(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] <= key) L = mid + 1;
else R = mid - 1;
}
return R;
}
最后一个<key的元素的下标:
public int lastSmall(int[] arr, int key) {
int L = 0, R = arr.length - 1;
while(L <= R){
int mid = L + (R-L) / 2; // 防止直接相加后的溢出
if(arr[mid] < key) L = mid + 1;
else R = mid - 1;
}
return R;
}