算法模块
1. 链表
1. 链表翻转
// 递归
ListNode* reverseList(ListNode* head)
{
if(NULL == head || NULL == head->next)
return head;
ListNode * p = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return p;
}
// 非递归
ListNode* reverseList(ListNode* head) {
ListNode *curr = head;
if (curr == NULL) {
return NULL;
}
ListNode *prev = NULL, *temp = NULL;
while (curr != NULL) {
temp = curr->next;
curr->next = prev;
prev = curr;
curr = temp;
}
return prev;
}
2. 单链表判断是不是环+求环位置+求环长度
bool hasCycle(ListNode *head) {
if (head == nullptr) {
return false;
}
ListNode *fast,*slow;
slow = head;
fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return true;
}
}
return false;
}
以图片为例,假设环入口距离链表头的长度为L,快慢指针相遇的位置为cross
,且该位置距离环入口的长度为S。考虑快慢指针移动的距离,慢指针走了L+S
,快指针走了L+S+nR
(这是假设相遇之前快指针已经绕环n圈)。由于快指针的速度是慢指针的两倍,相同时间下快指针走过的路程就是慢指针的两倍,所以有2(L+S)=L+S+nR
,化简得L+S=nR
当n=1时,即快指针在相遇之前多走了一圈,即L+S=R,也就是L=R−S,观察片表示从链表头到环入口的距离,而R−S表示从cross继续移动到环入口的距离,既然二者是相等的,那么如果采用两个指针,一个从表头出发,一个从cross出发,那么它们将同时到达环入口。即二者相等时便是环入口节点
当n>1时,上式为L=nR−S,L仍然表示从链表头到达环入口的距离,而nR−S可以从cross出发移动nR步后再倒退S步,从cross移动nR步后回到cross位置,倒退S
步后是环入口,所以也是同时到达环入口。即二者相等时便是环入口节点
所以寻找环入口的方法就是采用两个指针,一个从表头出发,一个从相遇点出发,一次都只移动一步,当二者相等时便是环入口的位置.
ListNode *detectCycle(ListNode *head) {
if (head == nullptr) {
return nullptr;
}
ListNode *fast,*slow;
slow = head;
fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
ListNode *slow2 = head;
while (slow2 != slow) {
slow = slow->next;
slow2 = slow2->next;
}
return slow2;
}
}
return nullptr;
}
3. 两个链表相交 求交点
两个链表,从某个节点开始相交,找到相交节点
方法很多,简单列举一下
将其中一个链表的头尾相连,问题转化为求环入口节点
用两个栈分别记录两个链表的节点,再弹出,找到最后一个相等的节点
一个容易想到的方法是,先得到两个链表的长度,然后得到长度的差值 distance,两个指针分别从两个链表头部遍历,其中较长链表指针先走 distance 步,然后同时向后走,当两个指针相遇的时候,即链表的交点
int getListLength(ListNode *head) {
if (head == nullptr) {
return 0;
}
int length = 0;
ListNode *p = head;
while (p!=nullptr) {
p = p->next;
length ++;
}
return length;
}
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lengthA = getListLength(headA);
int lengthB = getListLength(headB);
if (lengthA > lengthB) {
std::swap(headA, headB);
};
int distance = abs(lengthB - lengthA);
ListNode *p1 = headA;
ListNode *p2 = headB;
while(distance--) {
p2 = p2->next;
}
while (p1 != nullptr && p2 != nullptr) {
if (p1 == p2)
return p1;
p1 = p1->next;
p2 = p2->next;
}
return NULL;
}
4. 单链表找中间节点
用快慢指针法,当快指针走到链表结尾时,慢指针刚好走到链表的中间
ListNode* middleNode(ListNode* head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
5. 单链表合并
两个链表本身都是排序过的,把两个链表从头节点开始,逐个节点开始进行比较,最后剩下的节点接到尾部
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) {
if (l1 == nullptr) {
return l2;
}
if (l2 == nullptr) {
return l1;
}
ListNode dummy(-1);
ListNode *p = &dummy;
for (; l1 && l2; p = p->next) {
if (l1->val < l2->val) {
p->next = l1;
l1 = l1->next;
} else {
p->next = l2;
l2 = l2->next;
}
}
p->next = l1 != nullptr ? l1 : l2;
return dummy.next;
}
2. 树
二叉树
:二叉树是有限个结点的集合,这个集合或者是空集,或者是由一个根结点和两株互不相交的二叉树组成,其中一株叫根的做左子树,另一棵叫做根的右子树。
二叉树的性质:
性质1:在二叉树中第 i 层的结点数最多为2^(i-1)(i ≥ 1)
性质2:高度为k的二叉树其结点总数最多为2^k-1( k ≥ 1)
性质3:对任意的非空二叉树 T ,如果叶结点的个数为 n0,而其度为 2 的结点数为 n2,则:n0 = n2 + 1
满二叉树:深度为k且有2^k -1个结点的二叉树称为满二叉树
完全二叉树:深度为 k 的,有n个结点的二叉树,当且仅当其每个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应,称之为完全二叉树。(除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点)
性质4:具有 n 个结点的完全二叉树的深度为 log2n + 1
注意:仅有前序和后序遍历,不能确定一个二叉树,必须有中序遍历的结果
堆
如果一棵完全二叉树的任意一个非终端结点的元素都不小于其左儿子结点和右儿子结点(如果有的话) 的元素,则称此完全二叉树为最大堆。
同样,如果一棵完全二叉树的任意一个非终端结点的元素都不大于其左儿子结点和右儿子结点(如果 有的话)的元素,则称此完全二叉树为最小堆。
最大堆的根结点中的元素在整个堆中是最大的;
最小堆的根结点中的元素在整个堆中是最小的。
哈弗曼树
定义:给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman tree)。
构造:假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:将w1、w2、…,wn看成是有 n 棵树的森林(每棵树仅有一个结点);
在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
从森林中删除选取的两棵树,并将新树加入森林;
重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
二叉排序树
二叉排序树(Binary Sort Tree)又称二叉查找树(Binary Search Tree),亦称二叉搜索树。
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
左、右子树也分别为二叉排序树;
没有键值相等的节点
二分查找的时间复杂度是O(log(n)),最坏情况下的时间复杂度是O(n)(相当于顺序查找)
B-树
B-树:B-树是一种非二叉的查找树, 除了要满足查找树的特性,还要满足以下结构特性:
一棵 m 阶的B-树:
树的根或者是一片叶子(一个节点的树),或者其儿子数在 2 和 m 之间。
除根外,所有的非叶子结点的孩子数在 m/2 和 m 之间。
所有的叶子结点都在相同的深度。
B-树的平均深度为logm/2(N)。执行查找的平均时间为O(logm);
Trie 树
Trie 树,又称前缀树,字典树, 是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
Trie 树查询和插入时间复杂度都是 O(n),是一种以空间换时间的方法。当节点树较多的时候,Trie 树占用的内存会很大。
Trie 树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。
二叉树遍历算法
二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有深度遍历和广度遍历,深度遍历有前序、中序以及后序三种遍历方法,广度遍历即我们平常所说的层次遍历。因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁,而对于广度遍历来说,需要其他数据结构的支撑,比如堆了。所以,对于一段代码来说,可读性有时候要比代码本身的效率要重要的多。
前序遍历:根结点 ---> 左子树 ---> 右子树
中序遍历:左子树---> 根结点 ---> 右子树
后序遍历:左子树 ---> 右子树 ---> 根结点
层次遍历:只需按层次遍历即可
前序遍历:1 2 4 5 7 8 3 6
中序遍历:4 2 7 5 8 1 3 6
后序遍历:4 7 8 5 2 6 3 1
层次遍历:1 2 3 4 5 6 7 8
2.2 二叉树遍历算法
public void preOrderTraverse1(TreeNode root) {
if (root != null) {
System.out.print(root.val+" ");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
public void inOrderTraverse1(TreeNode root) {
if (root != null) {
inOrderTraverse1(root.left);
System.out.print(root.val+" ");
inOrderTraverse1(root.right);
}
}
public void postOrderTraverse1(TreeNode root) {
if (root != null) {
postOrderTraverse1(root.left);
postOrderTraverse1(root.right);
System.out.print(root.val+" ");
}
}
// 其实深度遍历就是上面的前序、中序和后序。但是为了保证与广度优先遍历相照应,也写在这。代码也比较好理解,其实就是前序遍历
public void depthOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.print(node.val+" ");
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}