数据结构笔试面试题

链表

1.一个指针指向此单链表中间的一个节点,将该节点从单链表中删除。

A->B->C->D ------------->A->C->D

将B指向D,B中的数据换成C中的数据,C的指针删除

	public <T> void  deleteRandomNode(Node<T> current)
	{
		Node<T> next = current.next;
		if (next != null)
		{
			current.next = next.next;
			current.element = next.element;
			next.next = null;
		}
	}

2.判断一个单链表中是否有环,如何找到环的入口

是否有环:快慢指针,快指针走2步,慢指针走1步,若有环则相遇;

环入口:A点将快指针置原点,快、慢指针走1步,第二次相遇则是环入口enter

low = enter + enter2cross = n 步

fast = enter + enter2cross + k*L = 2n 步

再走n步,low和fast都回到A点,enter = n - enter2cross;

3.判断两个链表是否相交,如何找到第一个公共节点

解法一:将一个链表首尾相连,再遍历另外一个链表,若有环则说明相交,再找环入口

解法二:分别遍历两个链表,比较两个链表的最后一个节点是否相同,相同则相交;

找一个公共节点:

首先遍历两个链表得到它们的长度m和n,先让长的链表走|m-n|步,两个链表再一起遍历,找到第一个公共点。

复杂度O(m+n)

public <T> ListNode<T> findFirstCommonNode(ListNode<T> head1, ListNode<T> head2)
	{
		int length1 = getListLength(head1);
		int length2 = getListLength(head2);
		int lengthDif = length1 - length2;
		ListNode<T> headLong = head1;
		ListNode<T> headShort = head2;
		if (length2 > length1)
		{
			headLong = head2;
			headShort = head1;
			lengthDif = length2 - length1;
		}
		//长链表先走lengthdif步
		for(int i = 0; i < lengthDif; i++)
			headLong = headLong.next;
		
		//在两个链表一起遍历
		while(headLong != null && headShort != null && headLong != headShort)
		{
			headLong = headLong.next;
			headShort = headShort.next;
		}
		return headLong;
	}
	
	
	private  <T> int getListLength(ListNode<T> head)//O(n)
	{
		int length = 0;
		ListNode<T> node = head;
		while(node != null)
		{
			length++;
			node = node.next;
		}
		return length;
	}


4.输入一个链表的头结点,从尾到头反过来打印出每个节点的值。

分析:显式用栈或者递归

	public void printListReversingly(ListNode<Integer> head)
	{
		ListNode<Integer> node = head;
		Stack<ListNode<Integer>> stack = new Stack<ListNode<Integer>>();
		while (node != null)
		{
			stack.push(node);
			node = node.next;
		}
		while(!stack.empty())
		{
			node = stack.pop();
			System.out.println(node.value);
		}
	}

5.输入一个链表,输出该链表中倒数第k个节点。

分析:倒数第k个节点就是第n-k+1个节点,使用两个指针都指向第一个节点,n个节点总共需要走n-1步,第一个指针走k-1步,接着一起走n-k步;

	public <T> ListNode<T> findKthToTail(ListNode<T> head, int k)
	{
		if (head == null || k == 0)
		{
			return null;
		}
		
		ListNode<T> Ahead = head; 
		ListNode<T> Bhead = head;
		for(int i = 0; i < k - 1; i++)
		{
			if(Ahead.next != null)
				Ahead = Ahead.next;
			else
				return null;
		}
		
		while(Ahead.next != null)
		{
			Ahead = Ahead.next;
			Bhead = Bhead.next;
		}
		return Bhead;
	}

6.反转链表:定义一个函数,输入一个链表的头结点,反转该链表并输出翻转后链表的头结点。

分析:使用三个指针保存链表的前一个节点prev,先前节点cur和下一个节点next,每次遍历后都更新

public <T> ListNode<T> reverseList(ListNode<T> head)
	{
		ListNode<T> cur = head;
		ListNode<T> prev = null;
		while(cur != null)//head不为空
		{
			ListNode<T> next = cur.next;
			if (next == null)//cur是尾节点
				break;
			cur.next = prev;
			prev = cur;
			cur = next;
		}
		return cur;
	}
递归代码:

	public <T> ListNode<T> reverseListRecursive(ListNode<T> head)
	{
		ListNode<T> cur = head;
		if (cur == null || cur.next == null)
		{
			return cur;
		}
		
		ListNode<T> next = cur.next;
		ListNode<T> reverseRest = reverseList(next);
		next.next = cur;
		return reverseRest;
	}
递归调用栈:

cur = tail next = null -->返回

……

cur = head.next next = head.next.next

cur = head next = head.next

7.输入两个递增排序的链表,合并这两个链表并使新链表中的结点任然是按照递增排序的。

递归:

public <T extends Comparable<? super T>> ListNode<T> merge(
			ListNode<T> head1, ListNode<T> head2)
	{
		if (head1 == null)
		{
			return head2;
		}
		else if (head2 == null)
		{
			return head1;
		}
		
		ListNode<T> mergeHead = null;
		if (head1.value.compareTo(head2.value) < 0)
		{
			mergeHead = head1;
			mergeHead.next = merge(head1.next, head2);//recursive
		}
		else
		{
			mergeHead = head2;
			mergeHead.next = merge(head1, head2.next);
		}
		return mergeHead;
	}

8.复杂链表的复制,每个节点除了有一个next指向下一个节点外,还有一个sibling指向链表的任意节点或者null。

第一步:在原始链表的每个节点N后创建N'

A->A'->B->B'->C->C'

class ComplexListNode
{
	ComplexListNode next;
	ComplexListNode sibling;
	int value;
}
	public void cloneNodes(ComplexListNode head)
	{
		ComplexListNode node = head;
		while (node != null)
		{
			ComplexListNode cloned = new ComplexListNode(); 
			cloned.value = node.value;
			cloned.next = node.next;
			cloned.sibling = null;
			
			node.next = cloned;
			
			node = node.next;
		}
	}

第二步:设置复制出来的节点的sibling,若N指向S,则N‘指向S’

	public void connectSiblingNodes(ComplexListNode head)
	{
		ComplexListNode node = head;
		while(node != null)
		{
			ComplexListNode cloned = node.next;
			if (node.sibling != null)
			{
				cloned.sibling = node.sibling.next;
			}
			node = node.next;
		}
	}
第三步:将长链表拆分为两个链表,奇数点为原始链表,偶数点为复制链表。

public ComplexListNode reconnectNodes(ComplexListNode head)
	{
		ComplexListNode node = head;
		ComplexListNode cloneHead = null;
		ComplexListNode cloned = null;
		
		if (node != null)//克隆头结点
		{
			cloneHead = cloned = node.next;//A'
			node.next = cloned.next;//A->B
			node = node.next;//B
		}
		while(node != null)
		{
			cloned.next = node.next;//A'->B'
			cloned = cloned.next;//B'
			
			node.next = cloned.next;//B->C
			node = node.next;//C
		}
		return cloneHead;
	}
完整过程

	public ComplexListNode clone(ComplexListNode head)
	{
		cloneNodes(head);
		connectSiblingNodes(head);
		return reconnectNodes(head);
	}

9.约瑟夫环问题:0,1,...,n-1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字,求出这个圆圈里剩下的最后一个数字。

分析:用环形链表,每当链表遍历到末尾时,把迭代器移到链表的头部。因为LinkedList具体自动装箱拆箱功能,所以下标和数字会混淆,即remove(Object obj),和 remove(int index), 所以采用迭代器的方式

	public int lastRemaining(int n, int m)
	{
		if (n < 1 || m < 1)
		{
			return -1;
		}

		LinkedList<Integer> list = new LinkedList<Integer>();
		for (int i = 0; i < n; i++)
			list.add(i);//把元素全都存到链表中

		ListIterator<Integer> iterator = list.listIterator();//具有删除功能的iterator
		while (list.size() > 1)
		{
			for (int i = 1; i <= m; i++)
			{
				if (iterator.hasNext())
				{
					iterator.next();
				}
				else
				{
					iterator = list.listIterator();//迭代器到末尾了,需要重置
					iterator.next();
				}
			}
			iterator.remove();
		}
		return list.get(0);
	}


队列和栈

1.队列中取最大值

解法一:数组,链表实现O(N)

解法二:最大堆,O(1),入队和出队O(logN)

解法三:栈O(1),使用一个数组存储最大序列的下标

2.两个栈实现一个队列

EnQueue(v)
{
  stackB.push(v);
}
T DeQueue()
{
  if(stackA.empty())
  {
    while(!stackB.empty())
  {
    stackA.push(stackB.pop());
  }
}
  return stackA.pop();
}


3.定义一个包含min函数的栈,调用min、push及pop的时间复杂度都是O(1)

分析:使用数据栈和辅助栈,辅助栈保存每次的最小元素(之前的最小元素和新压入栈的元素的最小值)

class  StackWithMin <T extends Comparable<? super T>>
{
	private  Stack<T> dataStack = new Stack<T>();//数据栈
	private  Stack<T> minStack = new Stack<T>();//辅助栈
	public void push(T value)
	{
		dataStack.push(value);
		if (minStack.empty() || value.compareTo((minStack.peek())) < 0)
		{
			minStack.push(value);
		}
		else
			minStack.push(minStack.peek());
	}
	public T pop()
	{
		T ret = null;
		if (!minStack.empty() && !dataStack.empty())
		{
			ret = dataStack.pop();
			minStack.pop();
		}
		return ret;
	}
	
	public T min()
	{
		T ret = null;
		if (!minStack.empty() && !dataStack.empty())
		{
			ret = minStack.peek();
		}
		return ret;
	}
}

4.栈的压入、弹出序列:输入两个整数序列,第一序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。

分析:如果下一个弹出的数字刚好是栈顶元素,直接弹出;

如果下一个弹出的数字不在栈顶,把压栈序列中还没有入栈的数字压入栈,知道把下一个需要弹出的数字压入栈顶为止;

如果所有的数字都压入了栈仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列;

ie.1,2,3,4,5是入栈序列,4,5,3,2,1是一个可能出栈序列;4,3,5,1,2不可能是出栈序列

public boolean isPopOrder(int[] push, int[] pop)
	{
		boolean isPossible = false;
		if (push.length != pop.length)
		{
			return isPossible;
		}
		int i_push = 0, j_pop = 0, length = push.length;
		Stack<Integer> stack = new Stack<Integer>();
		
		while(i_push < length)
		{
			while(stack.empty() || stack.peek() != pop[j_pop])//下一个弹出的元素不在栈顶
			{
				if (i_push == length)
				{
					break;
				}
				stack.push(push[i_push]);
				i_push++;
			}
			
			if (stack.peek() != pop[j_pop])//没有找到下一个弹出的数字
			{
				break;
			}
			stack.pop();
			j_pop++;
		}
		
		if (stack.empty() && j_pop == length)//全部弹出
		{
			isPossible = true;
		}
		return isPossible;
	}

二叉树

1.求二叉树中节点的最大距离:距离为两个节点之间边的个数

tips:相距最远的两个节点一定是两个叶子节点或者是一个叶子节点到它的根节点

设第k颗子树中相距最远的两个节点:Uk和Vk,距离定义为d(Uk,Vk);树R中相距最远的两个点的距离为max{d(U1,V1),...,d(Uk,Vk),max1+max2+2}

递归:

class Node
{
	Node left;
	Node right;
	int nMaxLeft;
	int nMaxRight;
	char value;
}
	int nMaxLen = 0;
	public void findMaxLen(Node root)
	{
		if (root == null)
		{
			return;
		}
		if (root.left == null)
		{
			root.nMaxLeft = 0;
		}
		if (root.right == null)
		{
			root.nMaxRight = 0;
		}
		if (root.left != null)
		{
			findMaxLen(root.left);
		}
		if (root.right != null)
		{
			findMaxLen(root.right);
		}
		if (root.left != null)//根节点的左子树的最长距离
		{
			int tmp = 0;
			if (root.left.nMaxLeft > root.left.nMaxRight)
			{
				tmp = root.left.nMaxLeft;
			}
			else
			{
				tmp = root.left.nMaxRight;
			}
			root.nMaxLeft = tmp + 1;
		}
		if (root.right != null)//根节点的右子树的最长距离
		{
			int tmp = 0;
			if (root.right.nMaxLeft > root.right.nMaxRight)
			{
				tmp = root.right.nMaxLeft;
			}
			else
			{
				tmp = root.right.nMaxRight;
			}
			root.nMaxRight = tmp + 1;
		}
		if (root.nMaxLeft + root.nMaxRight > nMaxLen)
		{
			nMaxLen = root.nMaxLeft + root.nMaxRight;
		}	
	}

2.层序遍历二叉树

访问第k层时候,只需要知道第k-1层的节点信息,不需要从根节点遍历

使用游标cur记录当前访问节点和last记录当前层次的最后一个节点的下一个位置,当cur==last时,该层访问结束,换行并将last定位于新一行的末尾

非递归:

	public void PrintNodeByLevel(Node root)
	{
		if (root == null)
		{
			return;
		}
		ArrayList<Node> list = new ArrayList<Node>();
		list.add(root);
		int cur = 0, last = 1;
		while(cur < list.size())
		{
			last = list.size();//新的一行访问开始时,重新定位last于当前行最后一个节点的下一个位置
			while(cur < last)
			{
				System.out.print(list.get(cur).value);
				if (list.get(cur).left != null)//当前访问节点的左节点不为空则压入
				{
					list.add(list.get(cur).left);
				}
				if (list.get(cur).right != null)
				{
					list.add(list.get(cur).right);
				}
				cur++;
			}
			System.out.println();//该层访问结束,换行
			
		}
	}

或者使用队列保存节点更简单,缺点:不能换行

public void printNodeByLevel(Node root)
	{
		if (root == null)
		{
			return;
		}
		LinkedList<Node> list = new LinkedList<Node>();//模拟队列
		list.offerLast(root);//入队
		
		while(list.size() != 0)
		{
			Node node = list.pollFirst();
			System.out.print(node.value);
			
			if (node.left != null)
			{
				list.offerLast(node);
			}
			if (node.right != null)
			{
				list.offerLast(node);
			}
		}
	}


3.重新二叉树:输入二叉树的前序和中序遍历的结果,重建出该二叉树

	public <T> BinaryTreeNode<T> Construct(T[] preorder, int preStart,
			int preEnd, T[] inorder, int inStart, int inEnd)
	{
		if (preorder == null || inorder == null || preorder.length == 0
				|| inorder.length == 0)
		{
			return null;
		}
		T rootValue = preorder[0];
		BinaryTreeNode<T> root = new BinaryTreeNode<T>();
		root.value = rootValue;
		root.left = root.right = null;

		if (preStart == preEnd)
		{
			if (inStart == inEnd && preorder[preStart] == inorder[inStart])//只有一个元素并且相等
			{
				return root;
			}
			else
			{
				try
				{
					throw new Exception("Invalid input");
				}
				catch (Exception e)
				{
					e.printStackTrace();
				}
			}
		}
		
		int i = inStart;
		while(i <= inEnd && inorder[i] != rootValue)
		{
			i++;
		}
		
		if (i == inEnd && inorder[i] != rootValue)
		{
			try
			{
				throw new Exception("Invalid input");
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
		}
		
		
		int leftLength = i;
		if (leftLength > 0)//递归构造左子树
		{
			root.left = Construct(preorder, preStart + 1, leftLength, inorder, inStart, leftLength - 1);
		}
		if (leftLength < preEnd - preStart)
		{
			root.right = Construct(preorder, leftLength + 1, preEnd, inorder, leftLength + 1, inEnd);
		}
		return root;
	}


4.输入两棵二叉树A和B,判断B是不是A的子结构。

分析:利用递归的思想,第一步在树A中找到和B的根节点的值一样的节点R,第二步再判断树A中以R为根节点的子树是不是包含和树B一样的结构

public <T> boolean hasSubtree(BinaryTreeNode<T> root1, BinaryTreeNode<T> root2)
	{
		boolean ret = false;
		if (root1 != null && root2 != null)
		{
			if (root1.value == root2.value)
			{
				ret = doesTree1HaveTree2(root1, root2);
			}
			if (!ret)
			{
				ret = hasSubtree(root1.left, root2);
			}
			if (!ret)
			{
				ret = hasSubtree(root1.right, root2);
			}
		}
		return ret;
	}
	
	public <T> boolean doesTree1HaveTree2(BinaryTreeNode<T> root1, BinaryTreeNode<T> root2)
	{
		if (root2 == null)
		{
			return true;
		}
		if (root1 == null)
		{
			return false;
		}
		if (root1.value != root2.value)
		{
			return false;
		}
		
		return doesTree1HaveTree2(root1.left, root2.left) && doesTree1HaveTree2(root1.right, root2.right);
	}

5.二叉树的镜像:输入一个二叉树,该输出输出它的镜像

分析:先前序遍历树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点。

递归代码:

public <T> void mirror(BinaryTreeNode<T> root)
	{
		if (root == null)
		{
			return;
		}
		if (root.left == null && root.right == null)
		{
			return;
		}
		
		BinaryTreeNode<T> tmp = root.left;
		root.left = root.right;
		root.right = tmp;
		
		if (root.left != null)
		{
			mirror(root.left);
		}
		if (root.right != null)
		{
			mirror(root.right);
		}
	}


6.二叉搜索树:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历。假设输入的数组的任意两个数字各不相同

分析:最后一个数字是根节点的值,第一部分是左子树节点的值,它们都比根节点的值小;第二部分是右子树节点的值,它们都比根节点的值大。如5,7,6,9,11,10,8递归判断

public boolean isBinarySearchTree(int[] a, int left, int right)
	{
		int length = right - left + 1;
		int root = a[right];
		//二叉树中左子树的节点小于根节点
		int i = 0;
		for(; i < length ; i++)
		{
			if (a[i] > root)
			{
				break;
			}
		}
		//二叉树中右子树的节点大于根节点
		int j = i;
		for(; j < length; j++)
		{
			if (a[j] < root)
			{
				return false;//右子树中有节点小于根节点,不是二叉搜索树
			}
		}
		
		//递归判断左子树是不是二叉搜索树
		boolean leftIsBST = true;
		if (i > 0)
		{
			leftIsBST = isBinarySearchTree(a, left, i - 1);
		}
		//递归判断右子树是不是二叉搜索树
		boolean rightIsBST = true;
		if (j < length)
		{
			rightIsBST = isBinarySearchTree(a, j, right - 1);
		}
		return leftIsBST & rightIsBST;
	}

7.二叉树中和为某一值的路径:输入一个二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。

分析:栈保存路径点,变量保存路径点的和;

当用前序遍历的方式访问某一节点时,把该结点添加到路径,并累加该节点的值,如果该节点是叶节点并且路径中节点值的和刚好等于输入的整数,则当前的路径符合要求,打印出来。如果当前节点不是叶节点,则继续访问它的子节点。当前节点访问结束后,自动回溯到它的父节点。

public void findPath(BinaryTreeNode<Integer> root, int number)
	{
		if (root == null)
		{
			return;
		}
		int currentSum = 0;//保存路径的和
		LinkedList<Integer> list = new LinkedList<Integer>();//模拟栈保存路径
		findPath(root, number, list, currentSum);
	}
	
	public void findPath(BinaryTreeNode<Integer> root, int number, LinkedList<Integer> list, int currentSum)
	{
		currentSum += root.value;
		list.push(root.value);
		
		//如果是叶节点,并且路径上节点的和等于输入的值,打印出这条路径
		boolean isLeft = root.left == null && root.right == null;
		if (isLeft && currentSum == number)
		{
			for(int i = 0; i < list.size(); i++)
			{
				System.out.print(list.get(i));
			}
			System.out.println();
		}
		//如果不是叶节点,则遍历它的子节点
		if (root.left != null)
		{
			findPath(root.left, number, list, currentSum);
		}
		if (root.right != null)
		{
			findPath(root.right, number, list, currentSum);
		}
		list.pop();
	}


8.将二叉搜索树转换成一个排序的双向链表,要求不能创建任何新的节点,只能调整树中节点指针的指向。

如     10

   6      14     4--6--8--10--12--14--16中序遍历

 4   8   12  16

分析:原先指向左子节点的指针调整为链表中指向前一个节点的指针,原先指向右子节点的指针调整为链表中指向后一个节点的指针;根节点和左子树的最大一个节点连接起来,同时和右子树最小的一个节点连接起来。

public  <T> BinaryTreeNode<T> convert(BinaryTreeNode<T> root)
	{
		BinaryTreeNode<T> lastNodeInList = null;
		convert(root, lastNodeInList);
		
		//返回头结点
		BinaryTreeNode<T> head  = lastNodeInList;
		while(head != null && lastNodeInList != null)
		{
			head = head.left;
		}
		return head;
	}
	
	private  <T> void convert(BinaryTreeNode<T> node, BinaryTreeNode<T> lastNodeInList)
	{
		if (node == null)
		{
			return;
		}
		
		BinaryTreeNode<T> current = node;//左子树
		if (current.left != null)
		{
			convert(current.left, lastNodeInList);
		}
		
		current.left = lastNodeInList;//根节点
		if (lastNodeInList != null)
		{
			lastNodeInList.right = current;
		}
		lastNodeInList = current;
		
		if (current.right != null)//右子树
		{
			convert(current.right, lastNodeInList);
		}
	}
	

9.二叉树的深度,从根节点到叶节点依次经过的节点形成路的一条路径,最长路径的长度即为树的深度。

分析:使用后序遍历,求得左子树left和右子树的深度right,根的深度即为max(left,right) + 1;

	public <T> int getDepth(BinaryTreeNode<T> root)
	{
		if (root == null)
		{
			return 0;
		}
		
		int leftDepth = getDepth(root.left);
		int rightDepth = getDepth(root.right);
		return leftDepth > rightDepth ? (leftDepth + 1) : (rightDepth + 1); 
	}
扩展:判断一棵树是不是平衡树,平衡树任意节点的左子树和右子树的深度相差不超过1

分析:后序遍历,一边判断其左子树和右子树是不是平衡树,一边求深度,如果左子树和右子树都是平衡树,则其是平衡树

public <T> boolean isBalance(BinaryTreeNode<T> root)
	{
		if (root == null)
		{
			root.depth = 0;//树保存一个depth遍历记录深度信息
			return true;
		}
		int leftDepth = root.left.depth, rightDepth = root.right.depth;
		if (isBalance(root.left) && isBalance(root.right))
		{
			int diff = leftDepth - rightDepth;
			if (diff <= 1 && diff >= -1)
			{
				root.depth = 1 + (leftDepth > rightDepth ? leftDepth : rightDepth);
				return true;
			}
		}
		return false;
	}

10.判断一个二叉树是不是镜像对称的

自定义一个中序遍历,节点,右子树,左子树

<T> boolean isSymmetrical(BinaryTreeNode<T> root)
	{
		return isSysmetrical(root, root);
	}
	
	<T> boolean isSysmetrical(BinaryTreeNode<T> root1, BinaryTreeNode<T> root2)
	{
		if (root1 == null && root2 == null)
		{
			return true;
		}
		if (root1 == null || root2 == null)
		{
			return false;
		}
		if (root1.value != root2.value)
		{
			return false;
		}
		return isSysmetrical(root1.left, root2.right) && isSysmetrical(root1.right, root2.left);
	}














 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值