Let us define a function split(T,v) that takes in a tree, T and a value to split at, v. Suppose that each node of the tree stores its left child and right child in addition to the value at that node. Use the following algorithm:
First we check to see if the input tree is simply a leaf or not.
If T is not a leaf, compare the value of its root node, v' with v.
If v' < v then recursively call split on the left subtree. Store the values of the recursive call as L' (returned left tree), R' (returned right tree), and r (option type indicated if the value v was found or not). Construct the new right tree, newR = Node(R',v',R), and return (L',r,newR).
Else if v' > v then recursively call split on the right subtree. Store the values of the recursive call as L' (returned left tree), R' (returned right tree), and r (option type indicated if the value v was found or not). Construct the new left tree, newL = Node(L',v',L), and return (newL,r,R').
Else if v' = v, return L, SOME(v), R.
If T is a leaf, we must have reached the root of the tree without finding the input value v to split at. Return that you couldn't find the leaf by passing back NONE.
Why is this logarithmic? Well, you only ever traverse one root-to-leaf path of the tree, at most. We can easily reconstruct nodes in constant time since we're just re-assigning O(logn) references (in an imperative language) or reassigning O(logn) values that take a constant time to generate (in a functional language).
Here's the corresponding code for the algorithm. It's written in SML, but I'd be willing to clarify what anything means in the comments.
fun split(T,v) =
case T of
Leaf => (Leaf, NONE, Leaf)
| Node(L,v,R) =>
case compare(v, v') of
LESS =>
let
val (L',r,R') = split(L,k)
in
(L',r,Node(R',r,R))
end
| GREATER =>
let
val (L',r,R') = split(R,k)
in
(Node(L',v',L),r,R')
end
| EQUAL => (L, SOME(v), R)
See this document for more details. It provides a more thorough explanation of the above.